package fr.adrienbrault.idea.symfony2plugin.completion.command;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Processor;
import com.jetbrains.php.lang.PhpLanguage;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Symfony\Component\Console\Output\OutputInterface
*
* $input->getOption('<caret>'); $input->hasOption('<caret>')
* $input->getArgument('<caret>'); $input->hasArgument('<caret>')
*
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class PhpCommandGotoCompletionRegistrar implements GotoCompletionRegistrar {
@Override
public void register(GotoCompletionRegistrarParameter registrar) {
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement context = psiElement.getContext();
if (!(context instanceof StringLiteralExpression)) {
return null;
}
for(String s : new String[] {"Option", "Argument"}) {
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.StringParameterRecursiveMatcher(context, 0)
.withSignature("Symfony\\Component\\Console\\Input\\InputInterface", "get" + s)
.withSignature("Symfony\\Component\\Console\\Input\\InputInterface", "has" + s)
.match();
if(methodMatchParameter != null) {
PhpClass phpClass = PsiTreeUtil.getParentOfType(methodMatchParameter.getMethodReference(), PhpClass.class);
if(phpClass != null) {
return new CommandGotoCompletionProvider(phpClass, s);
}
}
}
return null;
});
}
/**
* Collect "Options" and "Arguments" on "configure" function
*/
private static class CommandGotoCompletionProvider extends GotoCompletionProvider {
final private PhpClass phpClass;
final private String addMethod;
public CommandGotoCompletionProvider(PhpClass phpClass, String addMethod) {
super(phpClass);
this.phpClass = phpClass;
this.addMethod = "add" + addMethod;
}
@NotNull
@Override
public Collection<LookupElement> getLookupElements() {
Collection<LookupElement> elements = new ArrayList<>();
Map<String, CommandArg> targets = getCommandConfigurationMap(phpClass, addMethod);
for(CommandArg key: targets.values()) {
LookupElementBuilder lookup = LookupElementBuilder.create(key.getName()).withIcon(Symfony2Icons.SYMFONY);
String description = key.getDescription();
if(description != null) {
if(description.length() > 25) {
description = StringUtils.abbreviate(description, 25);
}
lookup = lookup.withTypeText(description, true);
}
if(key.getDefaultValue() != null) {
lookup = lookup.withTailText("(" + key.getDefaultValue() + ")", true);
}
elements.add(lookup);
}
return elements;
}
@NotNull
@Override
public Collection<PsiElement> getPsiTargets(PsiElement element) {
PsiElement parent = element.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return Collections.emptyList();
}
String contents = ((StringLiteralExpression) parent).getContents();
if(StringUtils.isBlank(contents)) {
return Collections.emptyList();
}
Map<String, CommandArg> targets = getCommandConfigurationMap(phpClass, addMethod);
if(!targets.containsKey(contents)) {
return Collections.emptyList();
}
return Collections.singletonList(targets.get(contents).getTarget());
}
@Nullable
private String getParameterStringValue(@NotNull PsiElement[] parameters, int index) {
if(index >= parameters.length ) {
return null;
}
if(!(parameters[index] instanceof StringLiteralExpression)) {
return null;
}
String contents = ((StringLiteralExpression) parameters[index]).getContents();
if(StringUtils.isBlank(contents)) {
return null;
}
return contents;
}
private Map<String, CommandArg> getCommandConfigurationMap(@NotNull PhpClass phpClass, final @NotNull String methodName) {
Method configure = phpClass.findMethodByName("configure");
if(configure == null) {
return Collections.emptyMap();
}
Collection<PsiElement> psiElements = PhpElementsUtil.collectMethodElementsWithParents(configure, new CommandDefPsiElementFilter(methodName));
if(psiElements.size() == 0) {
return Collections.emptyMap();
}
Map<String, CommandArg> targets = new HashMap<>();
for (PsiElement element : psiElements) {
if(!(element instanceof MethodReference)) {
continue;
}
/*
->setDefinition(new InputArgument())
->setDefinition(array(
new InputArgument(),
new InputOption(),
));
*/
if("setDefinition".equals(((MethodReference) element).getName())) {
Collection<NewExpression> newExpressions = PsiTreeUtil.collectElementsOfType(element, NewExpression.class);
for (NewExpression newExpression : newExpressions) {
if(methodName.equals("addOption") && PhpElementsUtil.getNewExpressionPhpClassWithInstance(newExpression, "Symfony\\Component\\Console\\Input\\InputOption") != null) {
// new InputOption()
PsiElement[] parameters = newExpression.getParameters();
String contents = getParameterStringValue(parameters, 0);
if(contents != null && StringUtils.isNotBlank(contents)) {
targets.put(contents, new CommandArg(parameters[0], contents, getParameterStringValue(parameters, 3), getParameterStringValue(parameters, 4)));
}
} else if(methodName.equals("addArgument") && PhpElementsUtil.getNewExpressionPhpClassWithInstance(newExpression, "Symfony\\Component\\Console\\Input\\InputArgument") != null) {
// new InputArgument()
PsiElement[] parameters = newExpression.getParameters();
String contents = getParameterStringValue(parameters, 0);
if(contents != null && StringUtils.isNotBlank(contents)) {
targets.put(contents, new CommandArg(parameters[0], contents, getParameterStringValue(parameters, 2), getParameterStringValue(parameters, 3)));
}
}
}
} else {
/*
->addArgument('arg3', null, 'desc')
->addOption('opt1', null, null, 'desc', 'default')
*/
PsiElement[] parameters = ((MethodReference) element).getParameters();
if(parameters.length > 0 && parameters[0] instanceof StringLiteralExpression) {
String contents = ((StringLiteralExpression) parameters[0]).getContents();
if(StringUtils.isNotBlank(contents)) {
if(methodName.equals("addOption")) {
targets.put(contents, new CommandArg(parameters[0], contents, getParameterStringValue(parameters, 3), getParameterStringValue(parameters, 4)));
} else if(methodName.equals("addArgument")) {
targets.put(contents, new CommandArg(parameters[0], contents, getParameterStringValue(parameters, 2), getParameterStringValue(parameters, 3)));
}
}
}
}
}
return targets;
}
private static class CommandDefPsiElementFilter implements Processor<PsiElement> {
private final String methodName;
public CommandDefPsiElementFilter(String methodName) {
this.methodName = methodName;
}
@Override
public boolean process(PsiElement psiElement) {
if(!(psiElement instanceof MethodReference)) {
return false;
}
String name = ((MethodReference) psiElement).getName();
return methodName.equals(name) || "setDefinition".equals(name);
}
}
}
private static class CommandArg {
@NotNull
private final PsiElement target;
private final String name;
private String description;
private String defaultValue;
public CommandArg(@NotNull PsiElement target, @NotNull String name) {
this.target = target;
this.name = name;
}
private CommandArg(@NotNull PsiElement target, @NotNull String name, @Nullable String description, @Nullable String defaultValue) {
this.target = target;
this.name = name;
this.description = description;
this.defaultValue = defaultValue;
}
@NotNull
public String getName() {
return name;
}
@Nullable
public String getDescription() {
return description;
}
@Nullable
public String getDefaultValue() {
return defaultValue;
}
@NotNull
public PsiElement getTarget() {
return target;
}
}
}