package org.elixir_lang.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.lang.folding.NamedFoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import org.elixir_lang.psi.*;
import org.elixir_lang.psi.call.Call;
import org.elixir_lang.psi.operation.Type;
import org.elixir_lang.psi.operation.infix.Normalized;
import org.elixir_lang.reference.ModuleAttribute;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static org.elixir_lang.psi.call.name.Function.*;
import static org.elixir_lang.psi.call.name.Module.KERNEL;
import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.*;
public class Builder extends FoldingBuilderEx {
/*
* CONSTANTS
*/
private static final String[] RESOLVED_FUNCTION_NAMES = new String[]{ALIAS, IMPORT, REQUIRE, USE};
/*
* Instance Methods
*/
/**
* Builds the folding regions for the specified node in the AST tree and its children.
*
* @param root the element for which folding is requested.
* @param document the document for which folding is built. Can be used to retrieve line
* numbers for folding regions.
* @param quick whether the result should be provided as soon as possible. Is true, when
* an editor is opened and we need to auto-fold something immediately, like Java imports.
* If true, one should perform no reference resolving and avoid complex checks if possible.
* @return the array of folding descriptors.
*/
@NotNull
@Override
public FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, final boolean quick) {
final List<FoldingDescriptor> foldingDescriptorList = new ArrayList<FoldingDescriptor>();
PsiTreeUtil.processElements(root,
new PsiElementProcessor() {
private Map<String, FoldingGroup> foldingGroupByModuleAttributeName =
new HashMap<String, FoldingGroup>();
/*
*
* Instance Methods
*
*/
/*
* Public Instance Methods
*/
@Override
public boolean execute(@NotNull PsiElement element) {
boolean keepProcessing = true;
if (element instanceof AtNonNumericOperation) {
keepProcessing = execute((AtNonNumericOperation) element);
} else if (element instanceof AtUnqualifiedNoParenthesesCall) {
keepProcessing = execute((AtUnqualifiedNoParenthesesCall) element);
} else if (element instanceof ElixirDoBlock) {
keepProcessing = execute((ElixirDoBlock) element);
} else if (element instanceof ElixirStabOperation) {
keepProcessing = execute((ElixirStabOperation) element);
} else if (element instanceof Call) {
keepProcessing = execute((Call) element);
}
return keepProcessing;
}
/*
* Private Instance Methods
*/
private boolean execute(@NotNull AtNonNumericOperation atNonNumericOperation) {
boolean keepProcessing = true;
if (!quick) {
keepProcessing = slowExecute(atNonNumericOperation);
}
return keepProcessing;
}
private boolean execute(@NotNull AtUnqualifiedNoParenthesesCall atUnqualifiedNoParenthesesCall) {
String moduleAttributeName = moduleAttributeName(atUnqualifiedNoParenthesesCall);
String name = moduleAttributeName.substring(1);
if (ModuleAttribute.isDocumentationName(name)) {
ElixirNoParenthesesOneArgument noParenthesesOneArgument =
atUnqualifiedNoParenthesesCall.getNoParenthesesOneArgument();
foldingDescriptorList.add(
new NamedFoldingDescriptor(
noParenthesesOneArgument.getNode(),
noParenthesesOneArgument.getTextRange(),
null,
"\"...\""
)
);
} else if (ModuleAttribute.isTypeName(name)) {
ElixirNoParenthesesOneArgument noParenthesesOneArgument =
atUnqualifiedNoParenthesesCall.getNoParenthesesOneArgument();
PsiElement[] children = noParenthesesOneArgument.getChildren();
if (children.length == 1) {
PsiElement child = children[0];
if (child instanceof Type) {
Type type = (Type) child;
PsiElement rightOperand = Normalized.rightOperand(type);
if (rightOperand != null) {
foldingDescriptorList.add(
new NamedFoldingDescriptor(
rightOperand.getNode(),
rightOperand.getTextRange(),
null,
"..."
)
);
}
}
}
}
return true;
}
private boolean execute(@NotNull Call call) {
for (String resolvedFunctionName : RESOLVED_FUNCTION_NAMES) {
if (call.isCalling(KERNEL, resolvedFunctionName)) {
if (isFirstInGroup(call, KERNEL, resolvedFunctionName)) {
Call last = lastInGroup(call, KERNEL, resolvedFunctionName);
PsiElement[] finalArguments = finalArguments(call);
if (finalArguments != null && finalArguments.length >= 1) {
TextRange textRange = new TextRange(
finalArguments[0].getTextOffset(),
last.getTextRange().getEndOffset()
);
foldingDescriptorList.add(
new NamedFoldingDescriptor(
call.getParent().getNode(),
textRange,
null,
"..."
)
);
}
}
}
}
return true;
}
private boolean execute(@NotNull ElixirDoBlock doBlock) {
foldingDescriptorList.add(new FoldingDescriptor(doBlock, doBlock.getTextRange()));
return true;
}
private boolean execute(@NotNull ElixirStabOperation stabOperation) {
int startOffset = stabOperation.operator().getTextOffset();
int endOffset = stabOperation.getTextRange().getEndOffset();
TextRange textRange = new TextRange(startOffset, endOffset);
foldingDescriptorList.add(new FoldingDescriptor(stabOperation, textRange));
return true;
}
private boolean isFirstInGroup(@NotNull Call call,
@NotNull String resolvedModuleName,
@NotNull String resolvedFunctionName) {
PsiElement previousSiblingExpression = previousSiblingExpression(call);
boolean first = true;
if (previousSiblingExpression instanceof Call) {
Call previousSiblingExpressionCall = (Call) previousSiblingExpression;
first = !previousSiblingExpressionCall.isCalling(resolvedModuleName, resolvedFunctionName);
}
return first;
}
@NotNull
private Call lastInGroup(@NotNull Call first,
@NotNull String resolvedModuleName,
@NotNull String resolvedFunctionName) {
PsiElement expression = first;
Call last = first;
while (true) {
expression = nextSiblingExpression(expression);
if (expression instanceof Call) {
Call call = (Call) expression;
if (call.isCalling(resolvedModuleName, resolvedFunctionName)) {
last = call;
continue;
}
}
break;
}
return last;
}
private boolean slowExecute(@NotNull AtNonNumericOperation atNonNumericOperation) {
boolean keepProcessing = true;
PsiReference reference = atNonNumericOperation.getReference();
if (reference != null) {
keepProcessing = slowExecute(atNonNumericOperation, reference);
}
return keepProcessing;
}
private boolean slowExecute(
@NotNull AtNonNumericOperation atNonNumericOperation,
@NotNull final AtUnqualifiedNoParenthesesCall atUnqualifiedNoParenthesesCall
) {
return slowExecute(
atNonNumericOperation,
atUnqualifiedNoParenthesesCall,
atUnqualifiedNoParenthesesCall.getNoParenthesesOneArgument().getText()
);
}
private boolean slowExecute(@NotNull AtNonNumericOperation atNonNumericOperation,
@NotNull PsiElement target) {
boolean keepProcessing = true;
if (target instanceof AtUnqualifiedNoParenthesesCall) {
keepProcessing = slowExecute(
atNonNumericOperation,
(AtUnqualifiedNoParenthesesCall) target
);
} else if (target instanceof QualifiableAlias) {
keepProcessing = slowExecute(
atNonNumericOperation,
(QualifiableAlias) target
);
}
return keepProcessing;
}
private boolean slowExecute(@NotNull AtNonNumericOperation atNonNumericOperation,
@NotNull PsiReference reference) {
PsiElement target = reference.resolve();
boolean keepProcessing = true;
if (target != null) {
keepProcessing = slowExecute(atNonNumericOperation, target);
}
return keepProcessing;
}
private boolean slowExecute(@NotNull AtNonNumericOperation atNonNumericOperation,
@NotNull final QualifiableAlias qualifiableAlias) {
return slowExecute(atNonNumericOperation, qualifiableAlias, qualifiableAlias.getName());
}
private boolean slowExecute(@NotNull AtNonNumericOperation atNonNumericOperation,
@NotNull PsiElement element,
@Nullable final String placeHolderText) {
String moduleAttributeName = atNonNumericOperation.moduleAttributeName();
FoldingGroup foldingGroup = foldingGroupByModuleAttributeName.get(moduleAttributeName);
if (foldingGroup == null) {
foldingGroup = FoldingGroup.newGroup(moduleAttributeName);
foldingGroupByModuleAttributeName.put(moduleAttributeName, foldingGroup);
}
foldingDescriptorList.add(
new FoldingDescriptor(
atNonNumericOperation.getNode(),
atNonNumericOperation.getTextRange(),
foldingGroup,
Collections.<Object>singleton(element)
) {
@Nullable
@Override
public String getPlaceholderText() {
return placeHolderText;
}
}
);
return true;
}
}
);
return foldingDescriptorList.toArray(new FoldingDescriptor[foldingDescriptorList.size()]);
}
/**
* Returns the text which is displayed in the editor for the folding region related to the
* specified node when the folding region is collapsed.
*
* @param node the node for which the placeholder text is requested.
* @return the placeholder text.
*/
@Nullable
@Override
public String getPlaceholderText(@NotNull ASTNode node) {
PsiElement element = node.getPsi();
String placeholderText = null;
if (element instanceof ElixirDoBlock) {
placeholderText = "do: ...";
} else if (element instanceof ElixirStabOperation) {
placeholderText = "-> ...";
}
return placeholderText;
}
/**
* Returns the default collapsed state for the folding region related to the specified node.
*
* @param node the node for which the collapsed state is requested.
* @return true if the region is collapsed by default, false otherwise.
*/
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
PsiElement element = node.getPsi();
boolean isCollapsedByDefault = false;
if (element instanceof AtNonNumericOperation) {
isCollapsedByDefault = ElixirFoldingSettings.getInstance().isReplaceModuleAttributesWithValues();
} else {
PsiElement[] children = element.getChildren();
for (PsiElement child : children) {
if (child instanceof Call) {
Call call = (Call) child;
for (String resolvedFunctionName : RESOLVED_FUNCTION_NAMES) {
if (call.isCalling(KERNEL, resolvedFunctionName)) {
isCollapsedByDefault = ElixirFoldingSettings
.getInstance()
.isCollapseElixirModuleDirectiveGroups();
break;
}
}
}
}
}
return isCollapsedByDefault;
}
}