/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.coldFusion.model.psi;
import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.coldFusion.UI.config.CfmlMappingsConfig;
import com.intellij.coldFusion.UI.config.CfmlProjectConfiguration;
import com.intellij.coldFusion.model.CfmlUtil;
import com.intellij.coldFusion.model.files.CfmlFile;
import com.intellij.coldFusion.model.psi.stubs.CfmlIndex;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.ResolveCache;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScopes;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author vnikolaenko
*/
// TODO: correctly deal with intersecting mappings (or show error?)
public class CfmlComponentReference extends CfmlCompositeElement implements CfmlReference, PlatformIcons {
private PsiElement myParent = null;
public CfmlComponentReference(@NotNull ASTNode node) {
super(node);
}
public CfmlComponentReference(@NotNull ASTNode node, PsiElement parent) {
this(node);
myParent = parent;
}
public String getComponentQualifiedName(String name) {
return getContainingFile().getComponentQualifiedName(name);
}
/*
private class PrefixSuffix {
// with dot separators
public String prefix;
// with file separators
public String suffix;
private PrefixSuffix(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
}
private List<String> getPossiblePathsForComponent(String componentQualifiedName) {
CfmlMappingsConfig mappings = CfmlProjectConfiguration.getInstance(getProject()).getMappings();
List<String> result = new LinkedList<String>();
StringBuilder prefix = new StringBuilder();
String suffix = File.separatorChar + componentQualifiedName.replace('.', File.separatorChar);
List<PrefixSuffix> ps = new LinkedList<PrefixSuffix>();
while (suffix != "") {
ps.add(new PrefixSuffix(prefix.toString(), suffix));
// recount prefix and suffix
int i = suffix.indexOf(File.separatorChar, 1);
if (i == -1) {
break;
}
String s = suffix.substring(1, i);
if (prefix.length() != 0) {
s = File.separatorChar + s;
}
prefix.append(s);
suffix = suffix.substring(i);
}
for (PrefixSuffix p : ps) {
String s = mappings.serverMappings.get(p.prefix);
if (s != null) {
result.add(s + p.suffix);
}
}
return result;
}
*/
/**
* @param componentQualifiedName
* @param originalFile = getContainingFile().getOriginalFile();
* @return
*/
public static Collection<CfmlComponent> resolveFromQualifiedName(String componentQualifiedName, @NotNull CfmlFile originalFile) {
List<CfmlComponent> result = new ArrayList<>();
if (componentQualifiedName == null) {
return result;
}
Project project = originalFile.getProject();
if (!componentQualifiedName.contains(".")) {
// resolve with directory scope
// PsiFile containingFile = getContainingFile();
// containingFile = containingFile == null ? null : containingFile.getOriginalFile();
{
CfmlFile cfmlConteiningFile = originalFile;
PsiDirectory directory = cfmlConteiningFile.getParent();
if (directory != null) {
GlobalSearchScope searchScope = GlobalSearchScopes.directoryScope(directory, false);
final Collection<CfmlComponent> components = CfmlIndex.getInstance(project).getComponentsByNameInScope(
componentQualifiedName, searchScope);
components.addAll(CfmlIndex.getInstance(project).getInterfacesByNameInScope(
componentQualifiedName, searchScope));
for (CfmlComponent component : components) {
result.add(component);
}
}
else {
final Collection<CfmlComponent> components = CfmlIndex.getInstance(project).getComponentsByName(
componentQualifiedName);
components.addAll(CfmlIndex.getInstance(project).getInterfacesByName(componentQualifiedName));
for (CfmlComponent component : components) {
result.add(component);
}
}
}
}
if (result.isEmpty()) {
String componentName = getComponentName(componentQualifiedName);
int i = componentQualifiedName.lastIndexOf(".");
String directoryName;
if (i == -1) {
directoryName = "";
}
else {
directoryName = componentQualifiedName.substring(0, i);
}
CfmlProjectConfiguration.State state = CfmlProjectConfiguration.getInstance(project).getState();
CfmlMappingsConfig mappings = state != null ? state.getMapps().clone() : new CfmlMappingsConfig();
adjustMappingsIfEmpty(mappings, originalFile.getProject());
// addFakeMappingsForResolution(mappings);
List<String> realPossiblePaths = mappings.mapVirtualToReal(directoryName);
// Collections.sort(realPossiblePaths);
final Collection<CfmlComponent> components = CfmlIndex.getInstance(project).getComponentsByName(
componentName);
components.addAll(CfmlIndex.getInstance(project).getInterfacesByName(
componentName));
for (CfmlComponent component : components) {
PsiDirectory parent = component.getContainingFile().getParent();
if (parent == null) {
continue;
}
VirtualFile virtualFile = parent.getVirtualFile();
for (String realPath : realPossiblePaths) {
if (FileUtil.toSystemIndependentName(realPath).equals(FileUtil.toSystemIndependentName(virtualFile.getPresentableUrl()))) {
result.add(component);
break;
}
}
}
for (String realPath : realPossiblePaths) {
VirtualFile fileByUrl = LocalFileSystem.getInstance().findFileByPath(realPath);
if (fileByUrl != null) {
PsiFile file = PsiManager.getInstance(project).findFile(fileByUrl);
if (file != null) {
PsiDirectory directory = file.getParent();
if (directory != null) {
GlobalSearchScope searchScope = GlobalSearchScopes.directoryScope(directory, false);
final Collection<CfmlComponent> componentsFromGlobalScope = CfmlIndex.getInstance(project).getComponentsByNameInScope(
componentName, searchScope);
componentsFromGlobalScope.addAll(CfmlIndex.getInstance(project).getInterfacesByNameInScope(
componentName, searchScope));
for (CfmlComponent component : componentsFromGlobalScope) {
result.add(component);
}
}
}
}
}
}
if (result.isEmpty()) {
final Couple<String> prefixAndName = CfmlUtil.getPrefixAndName(componentQualifiedName);
final String componentName = prefixAndName.getSecond();
final CfmlImport cfmlImport = CfmlUtil.getImportByPrefix(originalFile, prefixAndName.getFirst());
if (cfmlImport != null && !StringUtil.isEmpty(componentName)) {
String libtag = cfmlImport.getImportString();
final VirtualFile folder = CfmlUtil.findFileByLibTag(originalFile, libtag);
if (folder != null && folder.isDirectory()) {
final GlobalSearchScope scope = GlobalSearchScopes.directoryScope(originalFile.getProject(), folder, true);
result.addAll(CfmlIndex.getInstance(originalFile.getProject()).getComponentsByNameInScope(componentName, scope));
}
}
}
return result;
}
private final ResolveCache.PolyVariantResolver<CfmlComponentReference> MY_RESOLVER =
new ResolveCache.PolyVariantResolver<CfmlComponentReference>() {
@NotNull
public ResolveResult[] resolve(@NotNull final CfmlComponentReference expression, final boolean incompleteCode) {
String componentQualifiedName;
CfmlImport parentOfType = PsiTreeUtil.getParentOfType(expression, CfmlImport.class);
if (parentOfType != null) {
componentQualifiedName = getText();
}
else {
componentQualifiedName = getComponentQualifiedName(getText());
}
PsiFile containingFile = getContainingFile();
containingFile = containingFile == null ? null : containingFile.getOriginalFile();
if (containingFile instanceof CfmlFile) {
return CfmlResolveResult.create(resolveFromQualifiedName(componentQualifiedName, ((CfmlFile)containingFile)));
}
return ResolveResult.EMPTY_ARRAY;
}
};
@NotNull
public ResolveResult[] multiResolve(boolean incompleteCode) {
// incompleteCode = true, when autocompletion is executed,
// in this case, containingFile is not physical and there is no way to get parent directory
return MY_RESOLVER.resolve(this, incompleteCode);
// if(incompleteCode)
// return (getManager()).getResolveCache().resolveWithCaching(this, MY_RESOLVER, true, false);
/*
else
return MY_RESOLVER.resolve(this, incompleteCode);
*/
}
private static String getComponentName(@NotNull String componentName) {
int i = componentName.lastIndexOf('.');
if (i == -1) {
return componentName;
}
if (i == componentName.length() - 1) {
return "";
}
return componentName.substring(i + 1);
}
public PsiElement getElement() {
return myParent != null ? myParent : this;
}
public TextRange getRangeInElement() {
int offset = 0;
if (myParent != null) {
final int parentOffset = myParent.getTextRange().getStartOffset();
offset = getTextRange().getStartOffset() - parentOffset;
}
/*
final String referenceText = getCanonicalText();
final int index = referenceText.lastIndexOf(".");
if (index != -1) {
return new TextRange(index + 1, getTextLength()).shiftRight(offset);
}
*/
return new TextRange(0, getTextLength()).shiftRight(offset);
}
public PsiElement resolve() {
ResolveResult[] results = multiResolve(false);
if (results.length == 1) {
return results[0].getElement();
}
return null;
}
@NotNull
public String getCanonicalText() {
return getText();
}
public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
throw new IncorrectOperationException("Not implemented yet");
}
public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
throw new IncorrectOperationException("Not implemented yet");
}
public boolean isReferenceTo(PsiElement element) {
// TODO: replace with fully qualified names
if (element instanceof CfmlComponent && getCanonicalText().equals(((CfmlComponent)element).getName())) {
return true;
}
return false;
}
@NotNull
public Object[] getVariants() {
// final CfmlIndex cfmlIndex = CfmlIndex.getInstance(getProject());
return buildVariants(getText(), getContainingFile(), getProject(), this, true);
}
@NotNull
public static Object[] buildVariants(String text, PsiFile containingFile, final Project project,
@Nullable CfmlComponentReference reference,
final boolean forceQualify
) {
Collection<Object> variants = new THashSet<>();
String directoryName = "";
if (text.contains(".")) {
int i = text.lastIndexOf(".");
directoryName = text.substring(0, i);
}
CfmlProjectConfiguration.State state = CfmlProjectConfiguration.getInstance(project).getState();
CfmlMappingsConfig mappings = state != null ? state.getMapps().clone() : new CfmlMappingsConfig();
adjustMappingsIfEmpty(mappings, project);
if (reference != null) addFakeMappingsForImports(reference, mappings);
List<String> realPossiblePaths = mappings.mapVirtualToReal(directoryName);
for (String realPath : realPossiblePaths) {
addVariantsFromPath(variants, directoryName, realPath);
}
for (String value : mappings.getServerMappings().keySet()) {
if (value.startsWith(directoryName) && !value.isEmpty() && (StringUtil.startsWithChar(value, '/') ||
StringUtil.startsWithChar(value, '\\'))) {
variants.add(value.replace('\\', '.').replace('/', '.').substring(1));
}
}
containingFile = containingFile == null ? null : containingFile.getOriginalFile();
if (containingFile != null && containingFile instanceof CfmlFile) {
CfmlFile cfmlContainingFile = (CfmlFile)containingFile;
if (directoryName.length() == 0) {
PsiDirectory directory = cfmlContainingFile.getParent();
if (directory != null) {
addVariantsFromPath(variants, "", directory.getVirtualFile().getPresentableUrl());
}
}
else {
String dirPath = cfmlContainingFile.getParent().getVirtualFile().getPath() + "/" + directoryName.replaceAll("[./]+", "/");
addVariantsFromPath(variants, directoryName, dirPath);
}
}
final String finalDirectoryName = directoryName;
return ContainerUtil.map2Array(variants, new Function<Object, Object>() {
class DotInsertHandler implements InsertHandler<LookupElement> {
@Override
public void handleInsert(InsertionContext context, LookupElement item) {
Document document = context.getDocument();
int offset = context.getEditor().getCaretModel().getOffset();
document.insertString(offset, ".");
context.getEditor().getCaretModel().moveToOffset(offset + 1);
}
}
public Object fun(final Object object) {
if (object instanceof VirtualFile) {
VirtualFile element = (VirtualFile)object;
String elementNameWithoutExtension = element.getNameWithoutExtension();
String name = forceQualify ?
finalDirectoryName + (finalDirectoryName.length() == 0 ? "" : ".") + elementNameWithoutExtension :
elementNameWithoutExtension;
if (name.length() == 0) name = element.getName();
if (element.isDirectory()) {
return LookupElementBuilder.create(name).withIcon(DIRECTORY_CLOSED_ICON)
.withInsertHandler(new DotInsertHandler()).withCaseSensitivity(false);
}
else {
Icon icon = CLASS_ICON;
// choosing correct icon (class or interface)
if (CfmlIndex.getInstance(project).getComponentsByNameInScope(elementNameWithoutExtension, GlobalSearchScope
.fileScope(project, element)).size() == 1) {
icon = CLASS_ICON;
}
else if (CfmlIndex.getInstance(project).getInterfacesByNameInScope(elementNameWithoutExtension, GlobalSearchScope
.fileScope(project, element)).size() == 1) {
icon = INTERFACE_ICON;
}
return LookupElementBuilder.create(name).withIcon(icon).withCaseSensitivity(false);
}
}
else if (object instanceof String) {
return LookupElementBuilder.create((String)object).withInsertHandler(new DotInsertHandler()).withCaseSensitivity(false);
}
return object;
}
});
}
private static void adjustMappingsIfEmpty(CfmlMappingsConfig mappings, Project project) {
if (mappings.getServerMappings().size() != 0) {
return;
}
for (VirtualFile root : ProjectRootManager.getInstance(project).getContentRoots()) {
mappings.putToServerMappings("", root.getPresentableUrl());
}
}
private static void addFakeMappingsForImports(CfmlComponentReference ref, CfmlMappingsConfig mappings) {
if (PsiTreeUtil.getParentOfType(ref, CfmlImport.class) != null) {
// create fake mappings for imports
CfmlFile file = ref.getContainingFile();
Collection<String> importStrings = file.getImportStrings();
for (String importString : importStrings) {
final int index = importString.lastIndexOf('.');
if (index == -1) {
continue;
}
final String leftMapping = file.getComponentQualifiedName(importString).substring(0, index);
if (!StringUtil.isEmpty(leftMapping)) {
mappings.putToServerMappings("", leftMapping);
}
}
}
}
private static void addVariantsFromPath(Collection<Object> variants, String directoryName, String realPath) {
VirtualFile fileByUrl = LocalFileSystem.getInstance().findFileByPath(realPath);
if (fileByUrl != null) {
if (fileByUrl.isDirectory()) {
VirtualFile[] children = fileByUrl.getChildren();
for (VirtualFile child : children) {
if (child.isDirectory() || "cfc".equals(child.getExtension())) {
variants.add(child);
}
}
}
}
}
public boolean isSoft() {
// TODO: now - show no error if resolve failed, change when resolve will be fully implemented
return true;
}
@Override
public String getName() {
final String referenceText = getCanonicalText();
final int index = referenceText.lastIndexOf(".");
return referenceText != null ? referenceText.substring(index >= 0 ? (index + 1) : 0) : "";
}
@Override
public PsiType getPsiType() {
return null;
}
@Override
public PsiReference getReference() {
return this;
}
}