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, element1 -> pattern.accepts(element1)); 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)); } } } }