blob: fd4975f83d95e92b0ddcfe09fe10b59d40ee53e5 [file] [log] [blame]
package com.intellij.tasks.jira.jql.codeinsight;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.filters.ElementFilter;
import com.intellij.psi.filters.position.FilterPattern;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.tasks.jira.jql.JqlTokenTypes;
import com.intellij.tasks.jira.jql.psi.*;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.intellij.patterns.PlatformPatterns.psiElement;
/**
* @author Mikhail Golubev
*/
public class JqlCompletionContributor extends CompletionContributor {
private static final Logger LOG = Logger.getInstance(JqlCompletionContributor.class);
private static final FilterPattern BEGINNING_OF_LINE = new FilterPattern(new ElementFilter() {
@Override
public boolean isAcceptable(Object element, @Nullable PsiElement context) {
if (!(element instanceof PsiElement)) return false;
PsiElement p = (PsiElement)element;
PsiFile file = p.getContainingFile().getOriginalFile();
char[] chars = file.textToCharArray();
for (int offset = p.getTextOffset() - 1; offset >= 0; offset--) {
char c = chars[offset];
if (c == '\n') return true;
if (!StringUtil.isWhiteSpace(c)) return false;
}
return true;
}
@Override
public boolean isClassAcceptable(Class hintClass) {
return true;
}
});
private static FilterPattern rightAfterElement(final PsiElementPattern.Capture<? extends PsiElement> pattern) {
return new FilterPattern(new ElementFilter() {
@Override
public boolean isAcceptable(Object element, @Nullable PsiElement context) {
if (!(element instanceof PsiElement)) return false;
PsiElement prevLeaf = PsiTreeUtil.prevVisibleLeaf((PsiElement)element);
if (prevLeaf == null) return false;
PsiElement parent = PsiTreeUtil.findFirstParent(prevLeaf, new Condition<PsiElement>() {
@Override
public boolean value(PsiElement element) {
return pattern.accepts(element);
}
});
if (parent == null) return false;
if (PsiTreeUtil.hasErrorElements(parent)) return false;
return prevLeaf.getTextRange().getEndOffset() == parent.getTextRange().getEndOffset();
}
@Override
public boolean isClassAcceptable(Class hintClass) {
return true;
}
});
}
private static FilterPattern rightAfterElement(Class<? extends PsiElement> aClass) {
return rightAfterElement(psiElement(aClass));
}
// Patterns:
private static final PsiElementPattern.Capture<PsiElement> AFTER_CLAUSE_WITH_HISTORY_PREDICATE =
psiElement().and(rightAfterElement(JqlClauseWithHistoryPredicates.class));
private static final PsiElementPattern.Capture<PsiElement> AFTER_ANY_CLAUSE =
psiElement().andOr(
rightAfterElement(JqlTerminalClause.class),
// in other words after closing parenthesis
rightAfterElement(JqlSubClause.class));
private static final PsiElementPattern.Capture<PsiElement> AFTER_ORDER_KEYWORD =
psiElement().afterLeaf(psiElement(JqlTokenTypes.ORDER_KEYWORD));
private static final PsiElementPattern.Capture<PsiElement> AFTER_FIELD_IN_CLAUSE =
psiElement().and(rightAfterElement(
psiElement(JqlIdentifier.class).
andNot(psiElement().inside(JqlFunctionCall.class)).
andNot(psiElement().inside(JqlOrderBy.class))));
/**
* e.g. "not | ...", "status = closed and |" or "status = closed or |"
*/
private static final PsiElementPattern.Capture<PsiElement> BEGINNING_OF_CLAUSE = psiElement().andOr(
BEGINNING_OF_LINE,
psiElement().afterLeaf(psiElement().andOr(
psiElement().withElementType(JqlTokenTypes.AND_OPERATORS),
psiElement().withElementType(JqlTokenTypes.OR_OPERATORS),
psiElement().withElementType(JqlTokenTypes.NOT_OPERATORS).
andNot(psiElement().inside(JqlTerminalClause.class)),
psiElement().withElementType(JqlTokenTypes.LPAR).
andNot(psiElement().inside(JqlTerminalClause.class))
)));
/**
* e.g. "status changed on |"
*/
private static final PsiElementPattern.Capture<PsiElement> AFTER_KEYWORD_IN_HISTORY_PREDICATE = psiElement().
inside(JqlHistoryPredicate.class). // do not consider "by" inside "order by"
afterLeaf(psiElement().withElementType(JqlTokenTypes.HISTORY_PREDICATES));
/**
* e.g. "duedate > |" or "type was in |"
*/
private static final PsiElementPattern.Capture<PsiElement> AFTER_OPERATOR_EXCEPT_IS = psiElement().
inside(JqlTerminalClause.class).
afterLeaf(
psiElement().andOr(
psiElement().withElementType(JqlTokenTypes.SIMPLE_OPERATORS),
psiElement(JqlTokenTypes.WAS_KEYWORD),
psiElement(JqlTokenTypes.IN_KEYWORD),
// "not" is considered only as part of other complex operators
// "is" and "is not" are not suitable also
psiElement(JqlTokenTypes.NOT_KEYWORD).
afterLeaf(psiElement(JqlTokenTypes.WAS_KEYWORD))));
/**
* e.g. "foo is |" or "foo is not |"
*/
private static final PsiElementPattern.Capture<PsiElement> AFTER_IS_OPERATOR = psiElement().
inside(JqlTerminalClause.class).andOr(
psiElement().afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)),
psiElement().afterLeaf(psiElement(JqlTokenTypes.NOT_KEYWORD).
afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)))
);
/**
* e.g. "commentary ~ 'spam' order by |" or "assignee = currentUser() order by duedate desc, |"
*/
private static final PsiElementPattern.Capture<PsiElement> BEGINNING_OF_SORT_KEY = psiElement().
inside(JqlOrderBy.class).
andOr(
psiElement().afterLeaf(psiElement(JqlTokenTypes.COMMA)),
psiElement().afterLeaf(psiElement(JqlTokenTypes.BY_KEYWORD))
);
/**
* e.g. "status = 'in progress' order by reported |"
*/
private static final PsiElementPattern.Capture<PsiElement> AFTER_FIELD_IN_SORT_KEY = psiElement().
afterLeaf(psiElement().withElementType(JqlTokenTypes.VALID_FIELD_NAMES).inside(JqlSortKey.class));
private static final PsiElementPattern.Capture<PsiElement> INSIDE_LIST = psiElement().
inside(JqlList.class).
afterLeaf(
psiElement().andOr(
psiElement(JqlTokenTypes.LPAR),
psiElement(JqlTokenTypes.COMMA)
// e.g. assignee in ('mark', 'bob', currentUser() | )
).andNot(psiElement().inside(JqlFunctionCall.class))
);
public JqlCompletionContributor() {
addKeywordsCompletion();
addFieldNamesCompletion();
addFunctionNamesCompletion();
addEmptyOrNullCompletion();
}
@Override
public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
LOG.debug(DebugUtil.psiToString(parameters.getOriginalFile(), true));
super.fillCompletionVariants(parameters, result);
}
private void addKeywordsCompletion() {
extend(CompletionType.BASIC,
AFTER_ANY_CLAUSE,
new JqlKeywordCompletionProvider("and", "or", "order by"));
extend(CompletionType.BASIC,
AFTER_CLAUSE_WITH_HISTORY_PREDICATE,
new JqlKeywordCompletionProvider("on", "before", "after", "during", "from", "to", "by"));
extend(CompletionType.BASIC,
AFTER_FIELD_IN_CLAUSE,
new JqlKeywordCompletionProvider("was", "in", "not", "is", "changed"));
extend(CompletionType.BASIC,
psiElement().andOr(
BEGINNING_OF_CLAUSE,
psiElement().inside(JqlTerminalClause.class).andOr(
psiElement().afterLeaf(psiElement(JqlTokenTypes.WAS_KEYWORD)),
psiElement().afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)))),
new JqlKeywordCompletionProvider("not"));
extend(CompletionType.BASIC,
psiElement().afterLeaf(
psiElement().andOr(
psiElement(JqlTokenTypes.NOT_KEYWORD).
andNot(psiElement().afterLeaf(
psiElement(JqlTokenTypes.IS_KEYWORD))).
andNot(psiElement().withParent(JqlNotClause.class)),
psiElement(JqlTokenTypes.WAS_KEYWORD))),
new JqlKeywordCompletionProvider("in"));
extend(CompletionType.BASIC,
AFTER_ORDER_KEYWORD,
new JqlKeywordCompletionProvider("by"));
extend(CompletionType.BASIC,
AFTER_FIELD_IN_SORT_KEY,
new JqlKeywordCompletionProvider("asc", "desc"));
}
private void addFieldNamesCompletion() {
extend(CompletionType.BASIC,
psiElement().andOr(
BEGINNING_OF_CLAUSE,
BEGINNING_OF_SORT_KEY),
new JqlFieldCompletionProvider(JqlFieldType.UNKNOWN));
}
private void addFunctionNamesCompletion() {
extend(CompletionType.BASIC,
psiElement().andOr(
AFTER_OPERATOR_EXCEPT_IS,
INSIDE_LIST,
// NOTE: function calls can't be used as other functions arguments according to grammar
AFTER_KEYWORD_IN_HISTORY_PREDICATE),
new JqlFunctionCompletionProvider());
}
private void addEmptyOrNullCompletion() {
extend(CompletionType.BASIC,
AFTER_IS_OPERATOR,
new JqlKeywordCompletionProvider("empty", "null"));
}
private static class JqlKeywordCompletionProvider extends CompletionProvider<CompletionParameters> {
private final String[] myKeywords;
private JqlKeywordCompletionProvider(String... keywords) {
myKeywords = keywords;
}
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
for (String keyword : myKeywords) {
result.addElement(LookupElementBuilder.create(keyword).withBoldness(true));
}
}
}
private static class JqlFunctionCompletionProvider extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
JqlFieldType operandType;
boolean listFunctionExpected;
PsiElement curElem = parameters.getPosition();
JqlHistoryPredicate predicate = PsiTreeUtil.getParentOfType(curElem, JqlHistoryPredicate.class);
if (predicate != null) {
listFunctionExpected = false;
JqlHistoryPredicate.Type predicateType = predicate.getType();
switch (predicateType) {
case BEFORE:
case AFTER:
case DURING:
case ON:
operandType = JqlFieldType.DATE;
break;
case BY:
operandType = JqlFieldType.USER;
break;
// from, to
default:
operandType = findTypeOfField(curElem);
}
}
else {
operandType = findTypeOfField(curElem);
listFunctionExpected = insideClauseWithListOperator(curElem);
}
for (String functionName : JqlStandardFunction.allOfType(operandType, listFunctionExpected)) {
result.addElement(LookupElementBuilder.create(functionName)
.withInsertHandler(ParenthesesInsertHandler.NO_PARAMETERS));
}
}
private static JqlFieldType findTypeOfField(PsiElement element) {
JqlTerminalClause clause = PsiTreeUtil.getParentOfType(element, JqlTerminalClause.class);
if (clause != null) {
return JqlStandardField.typeOf(clause.getFieldName());
}
return JqlFieldType.UNKNOWN;
}
private static boolean insideClauseWithListOperator(PsiElement element) {
JqlTerminalClause clause = PsiTreeUtil.getParentOfType(element, JqlTerminalClause.class);
if (clause == null || clause.getType() == null) {
return false;
}
return clause.getType().isListOperator();
}
}
private static class JqlFieldCompletionProvider extends CompletionProvider<CompletionParameters> {
private final JqlFieldType myFieldType;
private JqlFieldCompletionProvider(JqlFieldType fieldType) {
myFieldType = fieldType;
}
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
for (String field : JqlStandardField.allOfType(myFieldType)) {
result.addElement(LookupElementBuilder.create(field));
}
}
}
}