package com.nvlad.yii2support.database; import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionResultSet; import com.intellij.codeInsight.completion.PrioritizedLookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.util.ProcessingContext; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.*; import com.nvlad.yii2support.common.ClassUtils; import com.nvlad.yii2support.common.DatabaseUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by oleg on 16.02.2017. */ public class QueryCompletionProvider extends com.intellij.codeInsight.completion.CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { MethodReference methodRef = ClassUtils.getMethodRef(completionParameters.getPosition(), 10); if (methodRef != null) { String prefix = completionResultSet.getPrefixMatcher().getPrefix(); char[] prefixes = {' ', ',', '.','[', '{', '%', '('}; completionResultSet = adjustPrefixes(prefixes, completionResultSet); Method method = (Method) methodRef.resolve(); if (method == null) { return; } int paramPosition = ClassUtils.indexForElementInParameterList(completionParameters.getPosition()); PhpClass phpClass = method.getContainingClass(); if (phpClass == null) return; PhpIndex index = PhpIndex.getInstance(method.getProject()); if ( (ClassUtils.isClassInheritsOrEqual(phpClass, "\\yii\\db\\Query", index) || ClassUtils.isClassInheritsOrEqual(phpClass,"\\yii\\db\\QueryTrait", index) || ClassUtils.isClassInherit(phpClass, "\\yii\\db\\BaseActiveRecord", index) || ClassUtils.isClassInheritsOrEqual(phpClass, "\\yii\\db\\Connection", index) || ClassUtils.isClassInheritsOrEqual(phpClass, "\\yii\\db\\Command", index) || ClassUtils.isClassInheritsOrEqual(phpClass,"\\yii\\db\\Migration", index) )) { PhpClass activeRecordClass = null; PhpClass possibleActiveRecordClass = ClassUtils.getPhpClassByCallChain(methodRef); if ( ClassUtils.isClassInherit(possibleActiveRecordClass, ClassUtils.getClass(index, "\\yii\\db\\BaseActiveRecord"))) activeRecordClass = possibleActiveRecordClass; // Calls inside ActiveQuery paired with ActiveRecord else if (ClassUtils.isClassInheritsOrEqual(possibleActiveRecordClass, ClassUtils.getClass(index, "\\yii\\db\\ActiveQuery"))) { if (possibleActiveRecordClass.getDocComment() != null) { activeRecordClass = ClassUtils.findClassInSeeTags(index, possibleActiveRecordClass, "\\yii\\db\\BaseActiveRecord"); } } /*----- ActiveQuery condition and column paramters ----*/ Project project = completionParameters.getPosition().getProject(); if (activeRecordClass != null && paramPosition >= 0 && method.getParameters().length > paramPosition && method.getParameters().length > 0 && (method.getParameters()[paramPosition].getName().equals("condition") || method.getParameters()[paramPosition].getName().equals("link") || method.getParameters()[paramPosition].getName().equals("sql") || method.getParameters()[paramPosition].getName().equals("on") || method.getParameters()[paramPosition].getName().equals("attributes") || method.getParameters()[paramPosition].getName().startsWith("column"))) { String tableName = null; // Override activeRecord and tableName for hasOne, hasMany and viaTable method if ((method.getName().equals("hasOne") || method.getName().equals("hasMany") || method.getName().equals("viaTable")) && method.getParameters()[paramPosition].getName().equals("link") && (completionParameters.getPosition().getParent().getParent().toString().equals("Array key") || completionParameters.getPosition().getParent().getParent().getParent() instanceof ArrayCreationExpression ) && paramPosition > 0 ) { if ( methodRef.getParameters()[paramPosition -1 ] instanceof StringLiteralExpression) { activeRecordClass = null; tableName = ClassUtils.removeQuotes( methodRef.getParameters()[paramPosition- 1].getText() ); } else { activeRecordClass = ClassUtils.getPhpClassUniversal(project,(PhpPsiElement) methodRef.getParameters()[paramPosition- 1]); } } if (activeRecordClass != null) tableName = getTable(prefix, activeRecordClass); if (tableName == null || tableName.isEmpty()) return; ArrayList<LookupElementBuilder> lookups = DatabaseUtils.getLookupItemsByTable(tableName, project, (PhpExpression) completionParameters.getPosition().getParent()); if (lookups != null && !lookups.isEmpty()) { addAllElementsWithPriority(lookups, completionResultSet, 2, true); // columns } else { ArrayList<LookupElementBuilder> items = DatabaseUtils.getLookupItemsByAnnotations(activeRecordClass, (PhpExpression) completionParameters.getPosition().getParent()); addAllElementsWithPriority(items, completionResultSet, 2, true); // fields } if (!isTabledPrefix(prefix)) { lookups = DatabaseUtils.getLookupItemsTables(project, (PhpExpression) completionParameters.getPosition().getParent()); if (lookups != null && lookups.size() == 0) lookups = DatabaseUtils.getLookupItemsByAnnotations(activeRecordClass, (PhpExpression) completionParameters.getPosition().getParent()); addAllElementsWithPriority(lookups, completionResultSet, 1); // tables } /*--- table parameter -----*/ } else if (method.getParameters().length > paramPosition && method.getParameters().length > 0 && ( method.getParameters()[paramPosition].getName().startsWith("table") || method.getParameters()[paramPosition].getName().startsWith("refTable" ) )) { // cancel codecompletion in case of "table" have , if (method.getParameters()[paramPosition].getName().equals("table") && methodRef.getParameters().length > paramPosition) { PsiElement element = methodRef.getParameters()[paramPosition]; String content = ClassUtils.getStringByElement(element); if (content.indexOf(',') >= 0) return; } if (!isTabledPrefix(prefix)) { ArrayList<LookupElementBuilder> lookups = DatabaseUtils.getLookupItemsTables(project, (PhpExpression) completionParameters.getPosition().getParent()); addAllElementsWithPriority(lookups, completionResultSet, 1); // tables } /*--- Query & Command -----*/ } else if (activeRecordClass == null && method.getParameters().length > paramPosition && (method.getParameters()[paramPosition].getName().equals("condition") || method.getParameters()[paramPosition].getName().startsWith("column") || method.getParameters()[paramPosition].getName().startsWith("refColumn") || method.getParameters()[paramPosition].getName().startsWith("sql")) ) { ArrayList<LookupElementBuilder> lookups = null; PhpExpression expr = (PhpExpression) completionParameters.getPosition().getParent(); if (( ClassUtils.isClassInheritsOrEqual(phpClass, ClassUtils.getClass(index, "\\yii\\db\\Command")) || ClassUtils.isClassInheritsOrEqual(phpClass, ClassUtils.getClass(index, "\\yii\\db\\Migration")) ) && paramPosition > 0) { PsiElement paramRef = methodRef.getParameters()[paramPosition - 1]; Parameter param = method.getParameters()[paramPosition - 1]; if (param.getName().equals("table") || param.getName().equals("refTable")) { String table = paramRef.getText(); if (table != null) { table = ClassUtils.removeQuotes(table); lookups = DatabaseUtils.getLookupItemsByTable(table, project, expr); } } } else if (isTabledPrefix(prefix)) { String table = getTable(prefix, null); lookups = DatabaseUtils.getLookupItemsByTable(table, project, expr); } else { lookups = DatabaseUtils.getLookupItemsTables(project, expr); } addAllElementsWithPriority(lookups, completionResultSet, 1); // tables } } } } @Nullable private String getTable(String stringToComplete, @Nullable PhpClass activeRecordClass) { if (stringToComplete.length() > 2 && stringToComplete.contains(".")) { // match "{{%table}}.[[co", "{{%table}}.[[", "{{%table}}.", "{{%table}}.col", "{{table}}.", "table.[[col", // "table.[[", "table.col" and "table." at end of string and return "tn" group with table name Pattern pattern = Pattern.compile(".*?((?<tn>[\\w-]+)(}{2})?)\\.((\\[\\[)?[\\w-]*)?$"); Matcher matcher = pattern.matcher(stringToComplete); if (matcher.matches()) { return matcher.group("tn"); } } if (activeRecordClass != null) { String tableName = DatabaseUtils.getTableByActiveRecordClass(activeRecordClass); if (tableName != null) { return DatabaseUtils.clearTablePrefixTags(ClassUtils.removeQuotes(tableName)); } } return null; } private boolean isTabledPrefix(String prefix) { // match "{{%table}}.[[co", "{{%table}}.[[", "{{%table}}.", "{{%table}}.col", "{{table}}.", "table.[[col", // "table.[[", "table.col" and "table." at end of string Pattern pattern = Pattern.compile("[\\w-]+(}{2})?\\.(\\[{2})?[\\w-]*?$"); Matcher matcher = pattern.matcher(prefix); return matcher.find(); } private CompletionResultSet adjustPrefixes(char[] prefixes, @NotNull CompletionResultSet completionResultSet) { for (char prefix: prefixes) { completionResultSet = adjustPrefix(prefix, completionResultSet); } return completionResultSet; } @NotNull private CompletionResultSet adjustPrefix(Character chr, @NotNull CompletionResultSet completionResultSet) { String currentColumn = completionResultSet.getPrefixMatcher().getPrefix(); if (currentColumn.indexOf(chr) != -1) { currentColumn = currentColumn.substring(currentColumn.lastIndexOf(chr) + 1).trim(); completionResultSet = completionResultSet.withPrefixMatcher(currentColumn); } return completionResultSet; } private void addAllElementsWithPriority(ArrayList<LookupElementBuilder> lookups, @NotNull CompletionResultSet completionResultSet, double priority) { addAllElementsWithPriority(lookups, completionResultSet, priority, false); } private void addAllElementsWithPriority(@Nullable ArrayList<LookupElementBuilder> lookups, @NotNull CompletionResultSet completionResultSet, double priority, boolean bold) { if (lookups != null) { for (LookupElementBuilder element : lookups) { element = element.withBoldness(bold); completionResultSet.addElement(PrioritizedLookupElement.withPriority(element, priority)); } } } }