package fr.adrienbrault.idea.symfony2plugin.config;
import com.intellij.codeInsight.daemon.LineMarkerInfo;
import com.intellij.codeInsight.daemon.LineMarkerProvider;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Settings;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.dic.ClassServiceDefinitionTargetLazyValue;
import fr.adrienbrault.idea.symfony2plugin.dic.XmlServiceParser;
import fr.adrienbrault.idea.symfony2plugin.doctrine.EntityHelper;
import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.util.DoctrineMetadataUtil;
import fr.adrienbrault.idea.symfony2plugin.form.util.FormUtil;
import fr.adrienbrault.idea.symfony2plugin.stubs.ServiceIndexUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.DoctrineModel;
import fr.adrienbrault.idea.symfony2plugin.util.resource.FileResourceUtil;
import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class ServiceLineMarkerProvider implements LineMarkerProvider {
@Nullable
@Override
public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
return null;
}
@Override
public void collectSlowLineMarkers(@NotNull List<PsiElement> psiElements, @NotNull Collection<LineMarkerInfo> results) {
// we need project element; so get it from first item
if(psiElements.size() == 0) {
return;
}
Project project = psiElements.get(0).getProject();
if(!Symfony2ProjectComponent.isEnabled(project)) {
return;
}
boolean phpHighlightServices = Settings.getInstance(project).phpHighlightServices;
for(PsiElement psiElement: psiElements) {
if(PhpElementsUtil.getMethodReturnPattern().accepts(psiElement)) {
this.formNameMarker(psiElement, results);
}
if(PhpElementsUtil.getClassNamePattern().accepts(psiElement)) {
this.classNameMarker(psiElement, results);
this.entityClassMarker(psiElement, results);
this.repositoryClassMarker(psiElement, results);
this.validatorClassMarker(psiElement, results);
this.constraintValidatorClassMarker(psiElement, results);
}
if(phpHighlightServices) {
collectNavigationMarkers(psiElement, results);
}
if(psiElement instanceof PhpFile) {
routeAnnotationFileResource((PhpFile) psiElement, results);
}
}
}
protected void collectNavigationMarkers(@NotNull PsiElement psiElement, Collection<? super RelatedItemLineMarkerInfo> result) {
if (!(psiElement instanceof StringLiteralExpression) || !(psiElement.getContext() instanceof ParameterList)) {
return;
}
ParameterList parameterList = (ParameterList) psiElement.getContext();
if (parameterList == null || !(parameterList.getContext() instanceof MethodReference)) {
return;
}
MethodReference method = (MethodReference) parameterList.getContext();
if(!new Symfony2InterfacesUtil().isContainerGetCall(method)) {
return;
}
String serviceId = ((StringLiteralExpression) psiElement).getContents();
String serviceClass = ServiceXmlParserFactory.getInstance(psiElement.getProject(), XmlServiceParser.class).getServiceMap().getMap().get(serviceId.toLowerCase());
if (null == serviceClass) {
return;
}
PsiElement[] resolveResults = PhpElementsUtil.getClassInterfacePsiElements(psiElement.getProject(), serviceClass);
if(resolveResults.length == 0) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.SERVICE_LINE_MARKER).
setTargets(resolveResults).
setTooltipText("Navigate to service");
result.add(builder.createLineMarkerInfo(psiElement));
}
private void classNameMarker(PsiElement psiElement, Collection<? super RelatedItemLineMarkerInfo> result) {
PsiElement phpClassContext = psiElement.getContext();
if(!(phpClassContext instanceof PhpClass)) {
return;
}
ClassServiceDefinitionTargetLazyValue psiElements = ServiceIndexUtil.findServiceDefinitionsLazy((PhpClass) phpClassContext);
if(psiElements == null) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.SERVICE_LINE_MARKER).
setTargets(psiElements).
setTooltipText("Navigate to definition");
result.add(builder.createLineMarkerInfo(psiElement));
}
private void entityClassMarker(PsiElement psiElement, Collection<? super RelatedItemLineMarkerInfo> result) {
PsiElement phpClassContext = psiElement.getContext();
if(!(phpClassContext instanceof PhpClass)) {
return;
}
Collection<PsiFile> psiFiles = new ArrayList<>();
// @TODO: use DoctrineMetadataUtil, for single resolve; we have collecting overhead here
for(DoctrineModel doctrineModel: EntityHelper.getModelClasses(psiElement.getProject())) {
PhpClass phpClass = doctrineModel.getPhpClass();
if(!PhpElementsUtil.isEqualClassName(phpClass, (PhpClass) phpClassContext)) {
continue;
}
PsiFile psiFile = EntityHelper.getModelConfigFile(phpClass);
// prevent self navigation for line marker
if(psiFile == null || psiFile instanceof PhpFile) {
continue;
}
psiFiles.add(psiFile);
}
if(psiFiles.size() == 0) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.DOCTRINE_LINE_MARKER).
setTargets(psiFiles).
setTooltipText("Navigate to model");
result.add(builder.createLineMarkerInfo(psiElement));
}
private void repositoryClassMarker(PsiElement psiElement, Collection<? super RelatedItemLineMarkerInfo> result) {
PsiElement phpClassContext = psiElement.getContext();
if(!(phpClassContext instanceof PhpClass)) {
return;
}
Collection<PsiFile> psiFiles = new ArrayList<>();
for (VirtualFile virtualFile : DoctrineMetadataUtil.findMetadataForRepositoryClass((PhpClass) phpClassContext)) {
PsiFile file = PsiManager.getInstance(psiElement.getProject()).findFile(virtualFile);
if(file == null) {
continue;
}
psiFiles.add(file);
}
if(psiFiles.size() == 0) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.DOCTRINE_LINE_MARKER).
setTargets(psiFiles).
setTooltipText("Navigate to metadata");
result.add(builder.createLineMarkerInfo(psiElement));
}
private void formNameMarker(PsiElement psiElement, Collection<? super RelatedItemLineMarkerInfo> result) {
if(!(psiElement instanceof StringLiteralExpression)) {
return;
}
Method method = PsiTreeUtil.getParentOfType(psiElement, Method.class);
if(method == null) {
return;
}
if(new Symfony2InterfacesUtil().isCallTo(method, "\\Symfony\\Component\\Form\\FormTypeInterface", "getParent")) {
// get form string; on blank string we dont need any further action
String contents = ((StringLiteralExpression) psiElement).getContents();
if(StringUtils.isBlank(contents)) {
return;
}
PsiElement formPsiTarget = FormUtil.getFormTypeToClass(psiElement.getProject(), contents);
if(formPsiTarget != null) {
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.FORM_TYPE_LINE_MARKER).
setTargets(formPsiTarget).
setTooltipText("Navigate to form type");
result.add(builder.createLineMarkerInfo(psiElement));
}
}
}
protected void routeAnnotationFileResource(@NotNull PsiFile psiFile, Collection<? super RelatedItemLineMarkerInfo> results) {
RelatedItemLineMarkerInfo<PsiElement> lineMarker = FileResourceUtil.getFileImplementsLineMarkerInFolderScope(psiFile);
if(lineMarker != null) {
results.add(lineMarker);
}
}
/**
* Constraints in same namespace and validateBy service name
*/
private void validatorClassMarker(PsiElement psiElement, Collection<LineMarkerInfo> results) {
PsiElement phpClassContext = psiElement.getContext();
if(!(phpClassContext instanceof PhpClass) || !PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Validator\\Constraint")) {
return;
}
Collection<PhpClass> phpClasses = new ArrayList<>();
// class in same namespace
String className = ((PhpClass) phpClassContext).getFQN() + "Validator";
phpClasses.addAll(
PhpElementsUtil.getClassesInterface(psiElement.getProject(), className)
);
// @TODO: validateBy alias
if(phpClasses.size() == 0) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.SYMFONY_LINE_MARKER).
setTargets(phpClasses).
setTooltipText("Navigate to validator");
results.add(builder.createLineMarkerInfo(psiElement));
}
/**
* "FooValidator" back to "Foo" constraint
*/
private void constraintValidatorClassMarker(PsiElement psiElement, Collection<LineMarkerInfo> results) {
PsiElement phpClass = psiElement.getContext();
if(!(phpClass instanceof PhpClass) || !PhpElementsUtil.isInstanceOf((PhpClass) phpClass, "Symfony\\Component\\Validator\\ConstraintValidatorInterface")) {
return;
}
String fqn = ((PhpClass) phpClass).getFQN();
if(fqn == null || !fqn.endsWith("Validator")) {
return;
}
Collection<PhpClass> phpClasses = new ArrayList<>(
PhpElementsUtil.getClassesInterface(psiElement.getProject(), fqn.substring(0, fqn.length() - "Validator".length()))
);
if(phpClasses.size() == 0) {
return;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.SYMFONY_LINE_MARKER).
setTargets(phpClasses).
setTooltipText("Navigate to constraint");
results.add(builder.createLineMarkerInfo(psiElement));
}
}