github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/hover.go (about)

     1  package langserver
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/thought-machine/please/src/parse/asp"
     9  	"github.com/thought-machine/please/tools/build_langserver/lsp"
    10  
    11  	"github.com/sourcegraph/jsonrpc2"
    12  )
    13  
    14  const hoverMethod = "textDocument/hover"
    15  
    16  func (h *LsHandler) handleHover(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) {
    17  	params, err := h.getParamFromTDPositionReq(req, hoverMethod)
    18  	if err != nil {
    19  		return nil, err
    20  	}
    21  	documentURI := params.TextDocument.URI
    22  
    23  	h.mu.Lock()
    24  	defer h.mu.Unlock()
    25  	content, err := h.getHoverContent(ctx, documentURI, params.Position)
    26  
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  
    31  	markedString := lsp.MarkedString{
    32  		Language: "build",
    33  		Value:    content,
    34  	}
    35  	markedStrings := []lsp.MarkedString{markedString}
    36  
    37  	log.Info("hover content: %s", markedStrings)
    38  
    39  	return &lsp.Hover{
    40  		Contents: markedStrings,
    41  		// TODO(bnmetrics): we can add range here later
    42  	}, nil
    43  }
    44  
    45  func (h *LsHandler) getHoverContent(ctx context.Context, uri lsp.DocumentURI, pos lsp.Position) (content string, err error) {
    46  	// Get the content of the line from the position
    47  	fileContent := h.workspace.documents[uri].textInEdit
    48  	if pos.Line >= len(fileContent) {
    49  		return "", &jsonrpc2.Error{
    50  			Code:    jsonrpc2.CodeInvalidParams,
    51  			Message: fmt.Sprintf("Invalid file line: requested %d, but we think file is only %d long", pos.Line, len(fileContent)),
    52  		}
    53  	}
    54  	lineContent := fileContent[pos.Line]
    55  
    56  	if err != nil {
    57  		return "", &jsonrpc2.Error{
    58  			Code:    jsonrpc2.CodeParseError,
    59  			Message: fmt.Sprintf("fail to read file %s: %s", uri, err),
    60  		}
    61  	}
    62  
    63  	// Return empty string if the hovered content is blank
    64  	if isEmpty(lineContent, pos) {
    65  		return "", nil
    66  	}
    67  	stmts := h.analyzer.AspStatementFromContent(JoinLines(fileContent, true))
    68  
    69  	call := h.analyzer.CallFromAST(stmts, pos)
    70  	label := h.analyzer.BuildLabelFromAST(ctx, stmts, uri, pos)
    71  	var contentString string
    72  	var contentErr error
    73  
    74  	if call != nil {
    75  		subincludes := h.analyzer.GetSubinclude(ctx, stmts, uri)
    76  		rule := h.analyzer.GetBuildRuleByName(call.Name, subincludes)
    77  
    78  		if rule != nil {
    79  			contentString, contentErr = contentFromCall(call.Arguments, rule, lineContent, pos)
    80  		}
    81  	}
    82  
    83  	if label != nil {
    84  		if label.BuildDef != nil && label.BuildDef.Content != "" {
    85  			contentString = label.BuildDef.Content
    86  		} else {
    87  			contentString = label.Definition
    88  		}
    89  	}
    90  
    91  	if contentErr != nil {
    92  		log.Warning("fail to get content from Build file %s: %s", uri, contentErr)
    93  		return "", nil
    94  	}
    95  	return contentString, nil
    96  }
    97  
    98  func contentFromCall(args []asp.CallArgument, ruleDef *RuleDef,
    99  	lineContent string, pos lsp.Position) (string, error) {
   100  
   101  	// check if the hovered content is on the name of the ident
   102  	if strings.Contains(lineContent, ruleDef.Name) {
   103  		// adding the trailing open paren to the identName ensure it's a call,
   104  		// prevent inaccuracy for cases like so: replace_str = x.replace('-', '_')
   105  		identNameIndex := strings.Index(lineContent, ruleDef.Name+"(")
   106  
   107  		if pos.Character >= identNameIndex &&
   108  			pos.Character <= identNameIndex+len(ruleDef.Name)-1 {
   109  			return contentFromRuleDef(ruleDef), nil
   110  		}
   111  	}
   112  
   113  	// Check arguments of the IdentStatement, and return the appropriate content if any
   114  	return contentFromArgs(args, ruleDef, pos)
   115  }
   116  
   117  func contentFromArgs(args []asp.CallArgument, ruleDef *RuleDef, pos lsp.Position) (string, error) {
   118  
   119  	for i, identArg := range args {
   120  		argNameEndPos := asp.Position{
   121  			Line:   identArg.Pos.Line,
   122  			Column: identArg.Pos.Column + len(identArg.Name),
   123  		}
   124  		if withInRange(identArg.Pos, argNameEndPos, pos) {
   125  			// This is to prevent cases like str.format(),
   126  			// When the positional args are not exactly stored in ArgMap
   127  			arg, okay := ruleDef.ArgMap[identArg.Name]
   128  			if okay {
   129  				return arg.Definition, nil
   130  			}
   131  			// Return definition if the hovered content is a positional argument
   132  		} else if identArg.Name == "" && withInRange(identArg.Value.Pos, identArg.Value.EndPos, pos) {
   133  			argInd := i
   134  			if ruleDef.Arguments[0].Name == "self" {
   135  				argInd++
   136  			}
   137  			return ruleDef.ArgMap[ruleDef.Arguments[argInd].Name].Definition, nil
   138  		}
   139  
   140  	}
   141  	return "", nil
   142  }
   143  
   144  // contentFromRuleDef returns the content from when hovering over the name of a function call
   145  // return value consist of a string containing the header and the docstring of a build rule
   146  func contentFromRuleDef(ruleDef *RuleDef) string {
   147  
   148  	header := ruleDef.Header
   149  	docString := ruleDef.Docstring
   150  
   151  	if docString != "" {
   152  		return header + "\n\n" + docString
   153  	}
   154  	return header
   155  }