org.eclipse.xtext.ide.editor.contentassist.antlr.ContentAssistContextFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.xtext.ide.editor.contentassist.antlr.ContentAssistContextFactory.java

Source

/*******************************************************************************
 * Copyright (c) 2014 itemis AG (http://www.itemis.eu) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.eclipse.xtext.ide.editor.contentassist.antlr;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.Token;
import org.apache.log4j.Logger;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.AbstractElement;
import org.eclipse.xtext.AbstractRule;
import org.eclipse.xtext.Action;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.GrammarUtil;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.ParserRule;
import org.eclipse.xtext.RuleCall;
import org.eclipse.xtext.ide.LexerIdeBindings;
import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext;
import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext.Builder;
import org.eclipse.xtext.ide.editor.contentassist.antlr.internal.Lexer;
import org.eclipse.xtext.ide.editor.partialEditing.IPartialEditingContentAssistParser;
import org.eclipse.xtext.nodemodel.BidiTreeIterator;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.ILeafNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.parser.antlr.ITokenDefProvider;
import org.eclipse.xtext.resource.DerivedStateAwareResource;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.ITextRegion;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.util.TextRegion;

import com.google.common.base.Function;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;

/**
 * @since 2.9
 */
public class ContentAssistContextFactory implements Function<ContentAssistContext.Builder, ContentAssistContext> {

    private static final Logger log = Logger.getLogger(ContentAssistContextFactory.class);

    private ExecutorService pool;

    @Inject
    protected IContentAssistParser parser;

    @Inject
    @Named(LexerIdeBindings.CONTENT_ASSIST)
    protected Lexer lexer;

    @Inject
    protected Provider<ContentAssistContext.Builder> contentAssistContextProvider;

    @Inject
    protected ITokenDefProvider tokenDefProvider;

    @Inject
    protected FollowElementComputer followElementComputer;

    protected XtextResource resource;

    protected ICompositeNode rootNode;

    protected INode lastCompleteNode;

    protected INode currentNode;

    protected INode lastVisibleNode;

    protected EObject currentModel;

    protected List<ContentAssistContext.Builder> contextBuilders;

    protected IParseResult parseResult;

    protected INode datatypeNode;

    protected int completionOffset;

    protected ITextRegion selection;

    private String document;

    @Override
    public ContentAssistContext apply(Builder from) {
        return from.toContext();
    }

    public ContentAssistContext[] create(String document, ITextRegion selection, int offset,
            XtextResource resource) {
        this.document = document;
        this.selection = selection;
        this.resource = resource;
        //This is called to make sure late initialization is done.
        if (resource instanceof DerivedStateAwareResource) {
            resource.getContents();
        }
        this.parseResult = resource.getParseResult();
        if (parseResult == null)
            throw new NullPointerException("parseResult is null");
        return doCreateContexts(offset);
    }

    protected INode getCurrentNode() {
        return currentNode;
    }

    public void setPool(ExecutorService pool) {
        this.pool = pool;
    }

    protected ContentAssistContext[] doCreateContexts(int offset) {
        initializeFromViewerAndResource(offset);
        List<Future<?>> futures = Lists.newArrayList();
        if (!datatypeNode.equals(lastCompleteNode)) {
            futures.add(pool.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    handleLastCompleteNodeAsPartOfDatatypeNode();
                    return null;
                }
            }));
        }

        // 2nd context: we assume, that the current token is incomplete and try to calculate
        // any valid grammar element by removing the current token and using it as prefix
        if (datatypeNode.equals(lastCompleteNode) && completionOffset != lastCompleteNode.getOffset()) {
            futures.add(pool.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    handleLastCompleteNodeIsAtEndOfDatatypeNode();
                    return null;
                }
            }));
        }

        // 4th context: we assume, that the current position is perfectly ok to insert a new token, if the previous one was valid
        if (!(lastCompleteNode instanceof ILeafNode) || lastCompleteNode.getGrammarElement() != null) {
            futures.add(pool.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    handleLastCompleteNodeIsPartOfLookahead();
                    return null;
                }
            }));
        }

        // wait for all requests to complete
        for (Future<?> f : futures) {
            try {
                f.get();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
        return Lists.transform(contextBuilders, this).toArray(new ContentAssistContext[contextBuilders.size()]);
    }

    protected void initializeFromViewerAndResource(int offset) {
        initializeAndAdjustCompletionOffset(offset);
        initializeNodeAndModelData();
        contextBuilders = Collections.synchronizedList(Lists.<ContentAssistContext.Builder>newArrayList());
    }

    protected void initializeNodeAndModelData() {
        rootNode = parseResult.getRootNode();
        if (parser instanceof IPartialEditingContentAssistParser) {
            ((IPartialEditingContentAssistParser) parser).initializeFor(resource.getEntryPoint());
        }
        lastCompleteNode = new LeafNodeFinder(completionOffset, true).searchIn(rootNode);
        if (lastCompleteNode == null)
            lastCompleteNode = rootNode;
        currentNode = new LeafNodeFinder(completionOffset, false).searchIn(rootNode);
        if (currentNode == null)
            currentNode = lastCompleteNode;
        lastVisibleNode = getLastCompleteNodeByOffset(rootNode, completionOffset);
        datatypeNode = getContainingDatatypeRuleNode(lastCompleteNode);
        currentModel = NodeModelUtils.findActualSemanticObjectFor(lastVisibleNode);
    }

    protected void initializeAndAdjustCompletionOffset(int offset) {
        completionOffset = offset;
        if (selection.getOffset() + selection.getLength() == offset)
            completionOffset = selection.getOffset();
    }

    protected void handleLastCompleteNodeIsPartOfLookahead() {
        // do not calculate twice for the same input
        if (!(lastCompleteNode instanceof ILeafNode && ((ILeafNode) lastCompleteNode).isHidden())) {
            createContextsForLastCompleteNode(currentModel, true);
        }
    }

    protected void handleLastCompleteNodeIsAtEndOfDatatypeNode() {
        String prefix = getPrefix(lastCompleteNode);
        String completeInput = document.substring(0, lastCompleteNode.getOffset());
        INode previousNode = getLastCompleteNodeByOffset(rootNode, lastCompleteNode.getOffset());
        EObject previousModel = previousNode.getSemanticElement();
        INode currentDatatypeNode = getContainingDatatypeRuleNode(currentNode);
        Collection<FollowElement> followElements = parser.getFollowElements(completeInput, false);
        int prevSize = contextBuilders.size();
        doCreateContexts(previousNode, currentDatatypeNode, prefix, previousModel, followElements);

        if (lastCompleteNode instanceof ILeafNode && lastCompleteNode.getGrammarElement() == null
                && contextBuilders.size() != prevSize) {
            handleLastCompleteNodeHasNoGrammarElement(contextBuilders.subList(prevSize, contextBuilders.size()),
                    previousModel);
        }
    }

    protected void handleLastCompleteNodeHasNoGrammarElement(List<Builder> contextBuilderToCheck,
            EObject previousModel) {
        List<ContentAssistContext> newContexts = Lists.transform(contextBuilderToCheck, this);
        boolean wasValid = isLikelyToBeValidProposal(lastCompleteNode, newContexts);
        if (wasValid && !(lastCompleteNode instanceof ILeafNode && ((ILeafNode) lastCompleteNode).isHidden())) {
            createContextsForLastCompleteNode(previousModel, false);
        }
    }

    protected void handleLastCompleteNodeAsPartOfDatatypeNode() {
        String prefix = getPrefix(datatypeNode);
        String completeInput = document.substring(0, datatypeNode.getOffset());
        Collection<FollowElement> followElements = parser.getFollowElements(completeInput, false);
        INode lastCompleteNodeBeforeDatatype = getLastCompleteNodeByOffset(rootNode, datatypeNode.getTotalOffset());
        doCreateContexts(lastCompleteNodeBeforeDatatype, datatypeNode, prefix, currentModel, followElements);
    }

    protected boolean isLikelyToBeValidProposal(INode lastCompleteNode, Iterable<ContentAssistContext> contexts) {
        for (ContentAssistContext context : contexts) {
            for (AbstractElement element : context.getFirstSetGrammarElements()) {
                if (element instanceof Keyword) {
                    String keywordValue = ((Keyword) element).getValue();
                    String lastText = ((ILeafNode) lastCompleteNode).getText();
                    if (keywordValue.equals(lastText))
                        return true;
                }
            }
        }
        return false;
    }

    protected void createContextsForLastCompleteNode(EObject previousModel, boolean strict) {
        String currentNodePrefix = getPrefix(currentNode);
        if (!Strings.isEmpty(currentNodePrefix) && !currentNode.getText().equals(currentNodePrefix)) {
            lexer.setCharStream(new ANTLRStringStream(currentNodePrefix));
            Token token = lexer.nextToken();
            if (token == Token.EOF_TOKEN) { // error case - nothing could be parsed
                return;
            }
            while (token != Token.EOF_TOKEN) {
                if (isErrorToken(token))
                    return;
                token = lexer.nextToken();
            }
        }
        String prefix = "";
        String completeInput = document.substring(0, completionOffset);
        Collection<FollowElement> followElements = parser.getFollowElements(completeInput, strict);
        doCreateContexts(lastCompleteNode, currentNode, prefix, previousModel, followElements);
    }

    /**
     * Return <code>true</code> if the token should be considered to be an error token.
     * If the token that is created from the prefix before the cursor position is an error
     * token, no proposals shall be computed that don't use a prefix. 
     * @return <code>true</true> if the token should be considered to be an error token.
     */
    protected boolean isErrorToken(Token token) {
        String tokenTypeName = tokenDefProvider.getTokenDefMap().get(token.getType());
        return "RULE_ANY_OTHER".equals(tokenTypeName);
    }

    protected void doCreateContexts(INode lastCompleteNode, INode currentNode, String prefix, EObject previousModel,
            Collection<FollowElement> followElements) {
        Set<AbstractElement> followElementAsAbstractElements = Sets.newLinkedHashSet();
        followElementComputer.computeFollowElements(followElements, followElementAsAbstractElements);
        Multimap<EObject, AbstractElement> contextMap = computeCurrentModel(previousModel, lastCompleteNode,
                followElementAsAbstractElements);
        currentNode = getContainingDatatypeRuleNode(currentNode);
        for (Entry<EObject, Collection<AbstractElement>> entry : contextMap.asMap().entrySet()) {
            ContentAssistContext.Builder contextBuilder = doCreateContext(lastCompleteNode, entry.getKey(),
                    previousModel, currentNode, prefix);
            for (AbstractElement element : entry.getValue()) {
                contextBuilder.accept(element);
            }
            contextBuilders.add(contextBuilder);
        }
    }

    protected Multimap<EObject, AbstractElement> computeCurrentModel(EObject currentModel, INode lastCompleteNode,
            Collection<AbstractElement> followElements) {
        Multimap<EObject, AbstractElement> result = LinkedHashMultimap.create();
        ICompositeNode currentParserNode = NodeModelUtils.getNode(currentModel);
        if (currentParserNode == null) {
            result.putAll(currentModel, followElements);
            return result;
        }
        EObject currentGrammarElement = currentParserNode.getGrammarElement();
        AbstractRule currentRule = getRule(currentGrammarElement);
        for (AbstractElement grammarElement : followElements) {
            AbstractRule rule = currentRule;
            ICompositeNode loopParserNode = currentParserNode;
            EObject loopLastGrammarElement = lastCompleteNode.getGrammarElement();
            while (!canBeCalledAfter(rule, loopLastGrammarElement, lastCompleteNode.getText(), grammarElement)
                    && loopParserNode.getParent() != null) {
                loopLastGrammarElement = loopParserNode.getGrammarElement();
                loopParserNode = loopParserNode.getParent();
                while (loopParserNode.getGrammarElement() == null && loopParserNode.getParent() != null)
                    loopParserNode = loopParserNode.getParent();
                EObject loopGrammarElement = loopParserNode.getGrammarElement();
                rule = getRule(loopGrammarElement);
            }
            EObject context = loopParserNode.getSemanticElement();
            result.put(context, grammarElement);
        }
        return result;
    }

    public INode getContainingDatatypeRuleNode(INode node) {
        INode result = node;
        EObject grammarElement = result.getGrammarElement();
        if (grammarElement != null) {
            ParserRule parserRule = GrammarUtil.containingParserRule(grammarElement);
            while (parserRule != null && GrammarUtil.isDatatypeRule(parserRule)) {
                result = result.getParent();
                grammarElement = result.getGrammarElement();
                parserRule = GrammarUtil.containingParserRule(grammarElement);
            }
        }
        return result;
    }

    protected int getCompletionOffset() {
        return completionOffset;
    }

    public ContentAssistContext.Builder doCreateContext(INode lastCompleteNode, EObject currentModel,
            EObject previousModel, INode currentNode, String prefix) {
        ContentAssistContext.Builder context = contentAssistContextProvider.get();

        context.setRootNode(rootNode);
        context.setLastCompleteNode(lastCompleteNode);
        context.setCurrentNode(currentNode);

        context.setRootModel(parseResult.getRootASTElement());
        context.setCurrentModel(currentModel);
        context.setPreviousModel(previousModel);
        context.setOffset(completionOffset);
        context.setPrefix(prefix);
        int regionLength = prefix.length();
        if (selection.getLength() > 0) {
            regionLength = regionLength + selection.getLength();
        }
        ITextRegion region = new TextRegion(completionOffset - prefix.length(), regionLength);
        if (selection.getOffset() >= 0 && selection.getLength() >= 0) {
            context.setSelectedText(
                    document.substring(selection.getOffset(), selection.getOffset() + selection.getLength()));
        }
        context.setReplaceRegion(region);
        context.setResource(resource);
        return context;
    }

    // TODO: match the node string with the widget string to compute the correct
    // replace region length
    public String getPrefix(INode prefixNode) {
        if (prefixNode instanceof ILeafNode) {
            if (((ILeafNode) prefixNode).isHidden() && prefixNode.getGrammarElement() != null)
                return "";
            return getNodeTextUpToCompletionOffset(prefixNode);
        }
        StringBuilder result = new StringBuilder(prefixNode.getTotalLength());
        doComputePrefix((ICompositeNode) prefixNode, result);
        return result.toString();
    }

    public String getNodeTextUpToCompletionOffset(INode currentNode) {
        int startOffset = currentNode.getOffset();
        int length = completionOffset - startOffset;
        String nodeText = ((ILeafNode) currentNode).getText();
        String trimmedNodeText = length > nodeText.length() ? nodeText : nodeText.substring(0, length);
        try {
            String text = document.substring(startOffset, startOffset + trimmedNodeText.length());
            if (trimmedNodeText.equals(text))
                return text;
            return document.substring(startOffset, startOffset + length);
        } catch (IndexOutOfBoundsException e) {
            log.error(e.getMessage(), e);
        }
        return trimmedNodeText;
    }

    public boolean doComputePrefix(ICompositeNode node, StringBuilder result) {
        List<ILeafNode> hiddens = Lists.newArrayListWithCapacity(2);
        for (INode child : node.getChildren()) {
            if (child instanceof ICompositeNode) {
                if (!doComputePrefix((ICompositeNode) child, result))
                    return false;
            } else {
                ILeafNode leaf = (ILeafNode) child;
                ITextRegion leafRegion = leaf.getTextRegion();
                if (leafRegion.getOffset() > completionOffset)
                    return false;
                if (leaf.isHidden()) {
                    if (result.length() != 0)
                        hiddens.add((ILeafNode) child);
                } else {
                    Iterator<ILeafNode> iter = hiddens.iterator();
                    while (iter.hasNext()) {
                        result.append(iter.next().getText());
                    }
                    hiddens.clear();
                    result.append(getNodeTextUpToCompletionOffset(leaf));
                    if (leafRegion.getOffset() + leafRegion.getLength() > completionOffset)
                        return false;
                }
            }
        }
        return true;
    }

    public void setParser(IContentAssistParser parser) {
        this.parser = parser;
    }

    public IContentAssistParser getParser() {
        return parser;
    }

    protected boolean canBeCalledAfter(AbstractRule rule, final EObject previousGrammarElement, String previousText,
            final EObject nextGrammarElement) {
        Boolean result = createCallHierachyHelper(previousGrammarElement, previousText, nextGrammarElement)
                .doSwitch(rule);
        return result;
    }

    protected CallHierarchyHelper createCallHierachyHelper(final EObject previousGrammarElement,
            final String previousText, final EObject nextGrammarElement) {
        return new CallHierarchyHelper(nextGrammarElement, previousText, previousGrammarElement);
    }

    protected AbstractRule getRule(EObject currentGrammarElement) {
        AbstractRule rule = null;
        if (currentGrammarElement instanceof RuleCall)
            rule = ((RuleCall) currentGrammarElement).getRule();
        if (currentGrammarElement instanceof AbstractRule)
            rule = (AbstractRule) currentGrammarElement;
        if (currentGrammarElement instanceof Action)
            rule = EcoreUtil2.getContainerOfType(currentGrammarElement, AbstractRule.class);
        if (rule == null)
            throw new IllegalStateException();
        return rule;
    }

    protected INode getLastCompleteNodeByOffset(INode node, int offsetPosition) {
        BidiTreeIterator<INode> iterator = node.getRootNode().getAsTreeIterable().iterator();
        INode result = node;
        while (iterator.hasNext()) {
            INode candidate = iterator.next();
            if (candidate.getOffset() >= offsetPosition) {
                break;
            } else if ((candidate instanceof ILeafNode) && (candidate.getGrammarElement() == null
                    || candidate.getGrammarElement() instanceof AbstractElement
                    || candidate.getGrammarElement() instanceof ParserRule)) {
                result = candidate;
            }
        }
        return result;
    }
}