/*
* Copyright 2011-present Greg Shrago
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.grammar;
import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.lookup.TailTypeDecorator;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ProcessingContext;
import org.intellij.grammar.generator.RuleGraphHelper;
import org.intellij.grammar.parser.GeneratedParserUtilBase;
import org.intellij.grammar.psi.*;
import org.intellij.grammar.psi.impl.GrammarUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Set;
import static com.intellij.patterns.PlatformPatterns.psiElement;
import static org.intellij.grammar.psi.BnfTypes.BNF_ID;
/**
* @author gregsh
*/
public class BnfCompletionContributor extends CompletionContributor {
public BnfCompletionContributor() {
PsiElementPattern.Capture<PsiElement> placePattern =
psiElement()
.inFile(PlatformPatterns.instanceOf(BnfFile.class))
.andNot(psiElement().inside(PsiComment.class));
extend(CompletionType.BASIC, placePattern, new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
PsiElement position = parameters.getPosition();
BnfCompositeElement parent = PsiTreeUtil.getParentOfType(position, BnfAttrs.class, BnfAttr.class, BnfParenExpression.class);
boolean attrCompletion;
if (parent instanceof BnfAttrs || isPossibleEmptyAttrs(parent)) {
attrCompletion = true;
}
else if (parent instanceof BnfAttr) {
BnfAttr attr = (BnfAttr)parent;
attrCompletion = position == attr.getId() || isOneAfterAnother(attr.getExpression(), position);
}
else {
attrCompletion = false;
}
if (attrCompletion) {
boolean inRule = PsiTreeUtil.getParentOfType(parent, BnfRule.class) != null;
ASTNode closingBrace = TreeUtil.findSiblingBackward(parent.getNode().getLastChildNode(), BnfTypes.BNF_RIGHT_BRACE);
attrCompletion = closingBrace == null || position.getTextOffset() <= closingBrace.getStartOffset();
if (attrCompletion) {
for (KnownAttribute attribute : KnownAttribute.getAttributes()) {
if (inRule && attribute.isGlobal()) continue;
result.addElement(LookupElementBuilder.create(attribute.getName()).withIcon(BnfIcons.ATTRIBUTE));
}
}
}
if (!attrCompletion && parameters.getInvocationCount() < 2) {
for (String keywords : suggestKeywords(parameters.getPosition())) {
result.addElement(TailTypeDecorator.withTail(LookupElementBuilder.create(keywords), TailType.SPACE));
}
}
}
});
extend(CompletionType.BASIC, placePattern.andNot(psiElement().inside(false, psiElement(BnfAttr.class))), new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull final CompletionResultSet result) {
BnfFile file = (BnfFile)parameters.getOriginalFile();
PsiElement positionRefOrToken = PsiTreeUtil.getParentOfType(parameters.getOriginalPosition(), BnfReferenceOrToken.class);
Set<String> explicitTokens = RuleGraphHelper.getTokenNameToTextMap(file).keySet();
for (String s : explicitTokens) {
result.addElement(LookupElementBuilder.create(s));
}
for (BnfRule rule : file.getRules()) {
for (BnfReferenceOrToken element : SyntaxTraverser.psiTraverser(rule.getExpression()).filter(BnfReferenceOrToken.class)) {
if (element == positionRefOrToken) continue;
if (element.resolveRule() == null) {
result.addElement(LookupElementBuilder.create(element.getText()));
}
}
}
}
});
}
@Override
public void beforeCompletion(@NotNull CompletionInitializationContext context) {
BnfFile file = ObjectUtils.tryCast(context.getFile(), BnfFile.class);
if (file == null) return;
int offset = context.getStartOffset();
PsiElement element = file.findElementAt(offset);
if (PsiUtil.getElementType(element) == BNF_ID) {
context.setDummyIdentifier("");
}
}
@Contract("null -> false")
private static boolean isPossibleEmptyAttrs(PsiElement attrs) {
if (!(attrs instanceof BnfParenExpression)) return false;
if (attrs.getFirstChild().getNode().getElementType() != BnfTypes.BNF_LEFT_BRACE) return false;
if (!(((BnfParenExpression) attrs).getExpression() instanceof BnfReferenceOrToken)) return false;
return isLastInRuleOrFree(attrs);
}
private static boolean isOneAfterAnother(@Nullable PsiElement e1, @Nullable PsiElement e2) {
if (e1 == null || e2 == null) return false;
return e1.getTextRange().getEndOffset() < e2.getTextRange().getStartOffset();
}
private static boolean isLastInRuleOrFree(PsiElement element) {
PsiElement parent = PsiTreeUtil.getParentOfType(element, BnfRule.class, GeneratedParserUtilBase.DummyBlock.class);
if (parent instanceof GeneratedParserUtilBase.DummyBlock) return true;
if (!(parent instanceof BnfRule)) return false;
for (PsiElement cur = element, next = cur.getNextSibling();
next == null || next instanceof PsiComment || next instanceof PsiWhiteSpace;
cur = next, next = cur.getNextSibling()) {
if (next == null) {
PsiElement curParent = cur.getParent();
while (next == null && curParent != parent) {
next = curParent.getNextSibling();
curParent = curParent.getParent();
}
if (curParent == parent) return true;
next = PsiTreeUtil.getDeepestFirst(next);
}
}
return false;
}
private static Collection<String> suggestKeywords(PsiElement position) {
TextRange posRange = position.getTextRange();
BnfFile posFile = (BnfFile)position.getContainingFile();
BnfRule statement = PsiTreeUtil.getTopmostParentOfType(position, BnfRule.class);
final TextRange range;
if (statement != null) {
range = new TextRange(statement.getTextRange().getStartOffset(), posRange.getStartOffset());
}
else {
int offset = posRange.getStartOffset();
for (PsiElement cur = GrammarUtil.getDummyAwarePrevSibling(position); cur != null; cur = GrammarUtil.getDummyAwarePrevSibling(cur)) {
if (cur instanceof BnfAttrs) offset = cur.getTextRange().getEndOffset();
else if (cur instanceof BnfRule) offset = cur.getTextRange().getStartOffset();
else continue;
break;
}
range = new TextRange(offset, posRange.getStartOffset());
}
String headText = range.substring(posFile.getText());
int completionOffset = StringUtil.isEmptyOrSpaces(headText)? 0 : headText.length();
String text = completionOffset == 0 ? CompletionInitializationContext.DUMMY_IDENTIFIER : headText;
GeneratedParserUtilBase.CompletionState state = new GeneratedParserUtilBase.CompletionState(completionOffset) {
@Override
public String convertItem(Object o) {
// we do not have other keywords
return o instanceof String? (String)o : null;
}
};
PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(posFile.getProject());
PsiFile file = psiFileFactory.createFileFromText("a.bnf", BnfLanguage.INSTANCE, text, true, false);
file.putUserData(GeneratedParserUtilBase.COMPLETION_STATE_KEY, state);
TreeUtil.ensureParsed(file.getNode());
if (completionOffset != 0) {
TextRange altRange = TextRange.create(posRange.getEndOffset(), Math.min(posRange.getEndOffset() + 100, posFile.getTextLength()));
String tailText = altRange.substring(posFile.getText());
String text2 = text + (StringUtil.isEmptyOrSpaces(tailText)? "a ::= " : tailText);
PsiFile file2 = psiFileFactory.createFileFromText("a.bnf", BnfLanguage.INSTANCE, text2, true, false);
file2.putUserData(GeneratedParserUtilBase.COMPLETION_STATE_KEY, state);
TreeUtil.ensureParsed(file2.getNode());
}
return state.items;
}
}