All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ai.vespa.schemals.lsp.schema.hover.SchemaHover Maven / Gradle / Ivy

There is a newer version: 8.441.21
Show newest version
package ai.vespa.schemals.lsp.schema.hover;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.Range;

import ai.vespa.schemals.SchemaLanguageServer;
import ai.vespa.schemals.context.EventPositionContext;
import ai.vespa.schemals.index.Symbol;
import ai.vespa.schemals.index.Symbol.SymbolStatus;
import ai.vespa.schemals.index.Symbol.SymbolType;
import ai.vespa.schemals.lsp.schema.completion.SchemaCompletion;
import ai.vespa.schemals.parser.indexinglanguage.ast.ATTRIBUTE;
import ai.vespa.schemals.parser.indexinglanguage.ast.INDEX;
import ai.vespa.schemals.parser.indexinglanguage.ast.SUMMARY;
import ai.vespa.schemals.schemadocument.resolvers.RankExpression.SpecificFunction;
import ai.vespa.schemals.tree.CSTUtils;
import ai.vespa.schemals.tree.Node;
import ai.vespa.schemals.tree.SchemaNode;
import ai.vespa.schemals.tree.Node.LanguageType;
import ai.vespa.schemals.tree.rankingexpression.RankNode;

/**
 * Responsible for LSP textDocument/hover requests.
 *
 * As a TODO it would have been nice with some highlighting on the generated Markdown.
 */
public class SchemaHover {
    private static Map> markdownContentCache = new HashMap<>();

    /**
     * Helper to check if a comment exists and extract it from a line
     * @param documentContent
     * @param lineEndOffset offset at which the \n following this line occurs
     * @return the string containing the comment, not including leading or trailing whitespace if found
     */
    private static Optional getCommentOnLine(String documentContent, int lineEndOffset) {
        int prevLine = documentContent.lastIndexOf("\n", lineEndOffset - 1);

        // note: if prevLine == -1 (not found), we should correctly extract from offset 0
        String possibleCommentString = documentContent.substring(prevLine + 1, lineEndOffset).trim();

        if (!possibleCommentString.startsWith("#")) return Optional.empty();
        return Optional.of(possibleCommentString);
    }

    /**
     * Some definitions might have useful comments by them.
     * This function takes a node which should point to some node where a symbol is defined,
     * and returns optionally a string containing all contiguous comments above the definition, joined by newline.
     */
    private static Optional getSymbolDocumentationComments(Node node, String documentContent) {
        int offset = node.getBeginOffset();
        if (offset == -1) return Optional.empty();

        int linePointer = documentContent.lastIndexOf("\n", offset);

        List comments = new LinkedList<>();
        while (linePointer != -1) {
            Optional comment = getCommentOnLine(documentContent, linePointer);
            if (comment.isEmpty()) break;

            comments.add(0, comment.get());
            linePointer = documentContent.lastIndexOf("\n", linePointer - 1);
        }

        if (comments.isEmpty()) return Optional.empty();

        return Optional.of(String.join("\n", comments));
    }

    /**
     * Special handling of struct over to list the fields inside and where they are inherited from.
     */
    private static Hover getStructHover(Node node, EventPositionContext context) {

        Optional structDefinitionSymbol = context.schemaIndex.getSymbolDefinition(node.getSymbol());
        
        if (structDefinitionSymbol.isEmpty()) {
            return null;
        }
        List fieldDefs = context.schemaIndex.listSymbolsInScope(structDefinitionSymbol.get(), SymbolType.FIELD);

        String result = "### struct " + structDefinitionSymbol.get().getShortIdentifier() + "\n";
        for (Symbol fieldDef : fieldDefs) {
            result += "    field " + fieldDef.getShortIdentifier() + " type " +  
                fieldDef.getNode().getNextSibling().getNextSibling().getText();

            if (!fieldDef.isInScope(structDefinitionSymbol.get())) {
                result += " (inherited from " + fieldDef.getScope().getPrettyIdentifier() + ")";
            }

            result += "\n";
        }

        if (result.isEmpty())result = "no fields found";

        return new Hover(new MarkupContent(MarkupKind.MARKDOWN, result));
    }

    /**
     * Special handling of field hover to also list which indexing settings are set (index | attribute | summary)
     * as well as whether the field is inside or outside its document.
     */
    private static Hover getFieldHover(Node node, EventPositionContext context) {
        Optional fieldDefinitionSymbol = context.schemaIndex.getSymbolDefinition(node.getSymbol());

        if (fieldDefinitionSymbol.isEmpty()) return null;

        Optional dataTypeNode = context.schemaIndex.fieldIndex().getFieldDataTypeNode(fieldDefinitionSymbol.get());
        
        String typeText = "unknown";
        if (dataTypeNode.isPresent()) {
            typeText = dataTypeNode.get().getText().trim();
        }

        // Create a nice long identifier for nested fields (e.g. field of type struct)
        String fieldIdentifier = fieldDefinitionSymbol.get().getShortIdentifier();
        Symbol scopeIterator = fieldDefinitionSymbol.get().getScope();
        while (scopeIterator != null && scopeIterator.getType() == SymbolType.FIELD) {
            fieldIdentifier = scopeIterator.getShortIdentifier() + "." + fieldIdentifier;
            scopeIterator = scopeIterator.getScope();
        }

        String hoverText = "field " + fieldIdentifier + " type " + typeText;

        if (context.schemaIndex.fieldIndex().getIsInsideDoc(fieldDefinitionSymbol.get())) {
            if (fieldDefinitionSymbol.get().getScope() != null && fieldDefinitionSymbol.get().getScope().getType() == SymbolType.STRUCT) {
                hoverText += " (in struct " + fieldDefinitionSymbol.get().getScope().getShortIdentifier() + ")";
            } else {
                hoverText += " (in document)";
            }
        } else {
            hoverText += " (outside document)";
        }

        var indexingTypes = context.schemaIndex.fieldIndex().getFieldIndexingTypes(fieldDefinitionSymbol.get()).toArray();
        // We simulate some kind of indexing statement
        for (int i = 0; i < indexingTypes.length; ++i) {
            if (i == 0) hoverText += "\n\t";
            else hoverText += " | ";
            hoverText += indexingTypes[i].toString().toLowerCase();
        }

        String fieldDefinitionDocumentContent = context.scheduler.getDocument(fieldDefinitionSymbol.get().getFileURI()).getCurrentContent();
        Optional documentationComments = getSymbolDocumentationComments(fieldDefinitionSymbol.get().getNode(), fieldDefinitionDocumentContent);

        if (documentationComments.isPresent()) {
            hoverText = documentationComments.get() + "\n" + hoverText;
        }

        // Render as code
        return new Hover(new MarkupContent(MarkupKind.MARKDOWN, "```\n" + hoverText + "\n```"));
    }

    private static Hover getSymbolHover(Node node, EventPositionContext context) {
        switch(node.getSymbol().getType()) {
            case STRUCT:
                return getStructHover(node, context);
            case FIELD:
                return getFieldHover(node, context);
            default:
                break;
        }

        if (node.getSymbol().getType() == SymbolType.LABEL) {
            return new Hover(new MarkupContent(MarkupKind.PLAINTEXT, "label"));
        }

        if (node.getSymbol().getStatus() == SymbolStatus.BUILTIN_REFERENCE) {
            return new Hover(new MarkupContent(MarkupKind.PLAINTEXT, "builtin"));
        }

        if (node.getSymbol().getStatus() == SymbolStatus.REFERENCE || node.getSymbol().getStatus() == SymbolStatus.DEFINITION) {
            Optional definition = context.schemaIndex.getSymbolDefinition(node.getSymbol());

            if (definition.isPresent()) {
                Node definitionNode = definition.get().getNode();

                if (definitionNode.getParent() != null) {
                    String text = definitionNode.getParent().getText();
                    String[] lines = text.split(System.lineSeparator());
                    String hoverContent = lines[0].trim();
                    while (hoverContent.length() > 0 && hoverContent.endsWith("{")) {
                        hoverContent = hoverContent.substring(0, hoverContent.length() - 1);
                    }
                    hoverContent = hoverContent.trim();
                    String documentContent = context.scheduler.getDocument(definition.get().getFileURI()).getCurrentContent();
                    Optional documentationComments = getSymbolDocumentationComments(definitionNode, documentContent);

                    if (documentationComments.isPresent()) {
                        hoverContent = documentationComments.get() + "\n" + hoverContent;
                    }
                    if (hoverContent.length() > 0) {
                        // Render as code
                        return new Hover(new MarkupContent(MarkupKind.MARKDOWN, "```\n" + hoverContent + "\n```"));
                    }
                }
            }
        }

        return null;
    }

    private static Hover getIndexingHover(SchemaNode hoverNode, EventPositionContext context) {
        // Taken from:
        // https://docs.vespa.ai/en/reference/schema-reference.html#indexing
        // Too specific to include in documentation fetcher IMO.
        
        // Note: these AST classes belong to the indexinglanguage parser
        if (hoverNode.isASTInstance(ATTRIBUTE.class)) {
            return new Hover(new MarkupContent(MarkupKind.MARKDOWN, 
                "## Attribute\n"
            +   "Attribute is used to make a field available for sorting, grouping, ranking and searching using match mode `word`.\n"
            ));
        } else if (hoverNode.isASTInstance(INDEX.class)) {
            return new Hover(new MarkupContent(MarkupKind.MARKDOWN, 
                "## Index\n"
            +   "Creates a searchable index for the values of this field using match mode `text`. "
            +   "All strings are lower-cased before stored in the index. "
            +   "By default, the index name will be the same as the name of the schema field. "
            +   "Use a fieldset to combine fields in the same set for searching.\n"
            ));
        } else if (hoverNode.isASTInstance(SUMMARY.class)) {
            return new Hover(new MarkupContent(MarkupKind.MARKDOWN, 
                "## Summary\n"
            +   "Includes the value of this field in a summary field. "
            +   "Summary fields of type string are limited to 64 kB. "
            +   "If a larger string is stored, the indexer will issue a warning and truncate the value to 64 kB. "
            +   "Modify summary output by using `summary:` in the field body (e.g. to generate dynamic teasers).\n"
            ));
        }

        return null;
    }

    private static Optional rankFeatureHover(SchemaNode node, EventPositionContext context) {

        // Search for rankNode connection
        Node currentNode = node;
        while (currentNode.getLanguageType() == LanguageType.RANK_EXPRESSION && currentNode.getSchemaNode().getRankNode().isEmpty()) {
            currentNode = currentNode.getParent();
        }

        SchemaNode schemaNode = currentNode.getSchemaNode();

        if (schemaNode.getRankNode().isEmpty()) {
            return Optional.empty();
        }

        RankNode rankNode = schemaNode.getRankNode().get();

        Optional rankNodeSignature = rankNode.getFunctionSignature();

        if (rankNodeSignature.isEmpty()) {
            return Optional.empty();
        }

        SpecificFunction functionSignature = rankNodeSignature.get().clone();

        // Clear property from the signature if the hover was not explicitly on the property node.
        if (node.hasSymbol() && node.getSymbol().getType() != SymbolType.PROPERTY) {
            functionSignature.clearProperty();
        }

        Optional result = getRankFeatureHover(functionSignature);
        result.ifPresent(hover -> hover.setRange(node.getRange()));
        return result;
    }

    /**
     * Public interface to be used by {@link SchemaCompletion}
     * @param function
     * @return Hover with empty range
     */
    public static Optional getRankFeatureHover(SpecificFunction function) {
        Optional result = getFileHoverInformation("rankExpression/" + function.getSignatureString(), new Range());

        if (result.isPresent()) {
            return result;
        }

        return getFileHoverInformation("rankExpression/" + function.getSignatureString(true), new Range());
    }

    public static Hover getHover(EventPositionContext context) {
        Node node = CSTUtils.getSymbolAtPosition(context.document.getRootNode(), context.position);
        
        if (node != null && node.isSchemaNode()) {
            SchemaNode schemaNode = node.getSchemaNode();
            Hover symbolHover = getSymbolHover(schemaNode, context);

            if (schemaNode.getLanguageType() == LanguageType.RANK_EXPRESSION) {
                var content = symbolHover.getContents();
                if (content.isRight()) {
                    MarkupContent markupContent = content.getRight();
                    if (markupContent.getValue() == "builtin") {

                        Optional builtinHover = rankFeatureHover(schemaNode, context);
                        if (builtinHover.isPresent()) {
                            return builtinHover.get();
                        }
                    }
                }
            }

            return symbolHover;
        }

        node = CSTUtils.getLeafNodeAtPosition(context.document.getRootNode(), context.position);
        if (node == null || !node.isSchemaNode()) return null;
        SchemaNode schemaNode = node.getSchemaNode();

        if (schemaNode.getLanguageType() == LanguageType.INDEXING) {
            return getIndexingHover(schemaNode, context);
        }

        Optional hoverInfo = getFileHoverInformation("schema/" + schemaNode.getClassLeafIdentifierString(), schemaNode.getRange());
        if (hoverInfo.isEmpty()) {
            return null;
        }
        return hoverInfo.get();
    }

    public static Optional getFileHoverInformation(String markdownKey, Range range) {
        // avoid doing unnecessary IO operations
        if (markdownContentCache.containsKey(markdownKey)) {
            Optional mdContent = markdownContentCache.get(markdownKey);
            if (mdContent != null) {
                if (mdContent.isEmpty()) {
                    return Optional.empty();
                }
                return Optional.of(new Hover(mdContent.get(), range));
            }
        }

        if (SchemaLanguageServer.serverPath == null)return Optional.empty();
        String fileName = markdownKey + ".md";

        Path markdownPath = SchemaLanguageServer.serverPath.resolve("hover").resolve(fileName);

        try {
            String markdown = Files.readString(markdownPath);

            MarkupContent mdContent = new MarkupContent(MarkupKind.MARKDOWN, markdown);
            Hover hover = new Hover(mdContent, range);
            markdownContentCache.put(markdownKey, Optional.of(mdContent));
            return Optional.of(hover);

        } catch(IOException ex) {
            return Optional.empty();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy