/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.completion.handlers;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.completion.PrefixMatcher;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.filters.ElementFilter;
import gw.lang.parser.IDynamicFunctionSymbol;
import gw.lang.parser.IParsedElement;
import gw.lang.parser.ISymbol;
import gw.lang.parser.ISymbolTable;
import gw.lang.parser.ITypeUsesMap;
import gw.lang.parser.Keyword;
import gw.lang.reflect.IBlockType;
import gw.lang.reflect.IEnumType;
import gw.lang.reflect.IType;
import gw.lang.reflect.gs.IGosuClass;
import gw.lang.reflect.gs.IGosuProgram;
import gw.lang.reflect.java.GosuTypes;
import gw.plugin.ij.completion.proposals.AdditionalSyntaxCompletionProposal;
import gw.plugin.ij.completion.proposals.RawCompletionProposal;
import gw.plugin.ij.completion.proposals.SymbolCompletionProposal;
import gw.plugin.ij.lang.parser.GosuRawPsiElement;
import gw.plugin.ij.lang.psi.IGosuPsiElement;
import gw.plugin.ij.lang.psi.impl.AbstractGosuClassFileImpl;
import gw.plugin.ij.lang.psi.impl.GosuFragmentFileImpl;
import gw.plugin.ij.lang.psi.impl.GosuProgramFileImpl;
import gw.plugin.ij.lang.psi.impl.statements.GosuForEachStatementImpl;
import gw.plugin.ij.lang.psi.impl.statements.GosuNotAStatementImpl;
import gw.plugin.ij.lang.psi.impl.statements.GosuStatementListImpl;
import gw.plugin.ij.lang.psi.impl.types.CompletionVoter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
/**
* Completes a symbol, not a .
*/
public class SymbolCompletionHandler extends AbstractCompletionHandler {
@NotNull
private final PrefixMatcher _prefixMatcher;
private IType _thisType;
public SymbolCompletionHandler(CompletionParameters params, @NotNull CompletionResultSet results) {
super(params, results);
_prefixMatcher = results.getPrefixMatcher();
}
public void handleCompletePath() {
final PsiElement position = _params.getOriginalPosition();
final PsiFile psiFile;
if (position == null) {
psiFile = _params.getOriginalFile();
} else {
psiFile = position.getContainingFile();
}
if (psiFile != null) {
final ISymbolTable symbolTable = ((AbstractGosuClassFileImpl) psiFile).getParseData().getSnapshotSymbols();
if (symbolTable != null) {
final ISymbol thisSymbol = symbolTable.getThisSymbolFromStackOrMap();
if (thisSymbol != null) {
_thisType = thisSymbol.getType();
}
int weight = 1;
// Second invocation of completion (Ctrl-Space)
boolean bSecondControlSpace = _params.getInvocationCount() >= 2;
if (isAllowed(psiFile, CompletionVoter.Type.SYMBOL)) {
maybeAddSymbols(symbolTable, weight--);
}
if (isAllowed(psiFile, CompletionVoter.Type.ENUM)) {
maybeAddEnumFields(weight--);
}
if (isAllowed(psiFile, CompletionVoter.Type.BLOCK)) {
maybeAddBlockCompletion(weight--);
}
if (isAllowed(psiFile, CompletionVoter.Type.KEYWORD)) {
maybeAddKeywords(weight--);
}
if (isAllowed(psiFile, CompletionVoter.Type.TYPE)) {
maybeAddTypes(weight--, bSecondControlSpace);
}
}
}
}
private boolean isAllowed(PsiFile psiFile, CompletionVoter.Type type) {
if(!(psiFile instanceof GosuFragmentFileImpl)) {
return true;
}
return psiFile instanceof CompletionVoter && ((CompletionVoter) psiFile).isCompletionAllowed(type);
}
private void maybeAddTypes( int weight, boolean bSecondControlSpace ) {
if (bSecondControlSpace ) {
// If it's the second Ctrl-Space, add ALL types
new TypeCompletionHandler(_params, getResult(),
new ElementFilter() {
@Override
public boolean isAcceptable(Object element, PsiElement context) {
String fqn;
if( element instanceof PsiClass ) {
PsiClass element1 = (PsiClass)element;
fqn = element1.getQualifiedName();
}
else if( element instanceof CharSequence ) {
fqn = element.toString();
}
else {
return false;
}
int iLastDot = fqn.lastIndexOf( '.' );
String simpleName = iLastDot > 0 ? fqn.substring( iLastDot + 1 ) : fqn;
return simpleName.startsWith( getResult().getPrefixMatcher().getPrefix() );
}
@Override
public boolean isClassAcceptable(Class hintClass) {
return true;
}
}, weight).handleCompletePath();
} else if( getTotalCompletions() == 0 ) {
// If there are no other completions available, add local types
PsiFile file = getContext().getPosition().getContainingFile();
if (file != null) {
if (_thisType instanceof IGosuClass) {
final ITypeUsesMap typeUsesMap = ((IGosuClass) _thisType).getTypeUsesMap();
if (typeUsesMap != null) {
new TypeCompletionHandler(_params, getResult(), weight).handleCompletePath();
}
}
}
}
}
private void maybeAddKeywords(int weight) {
for (AdditionalSyntaxCompletionProposal kw : getKeywordsThatMakeSense()) {
if (_prefixMatcher == null || _prefixMatcher.isStartMatch(kw.getGenericName())) {
kw.setWeight(weight);
addCompletion(kw);
}
}
}
@NotNull
public List<AdditionalSyntaxCompletionProposal> getKeywordsThatMakeSense() {
List<Keyword> keywords = makeExpressionKeywordList();
if (!parents(GosuNotAStatementImpl.class, GosuForEachStatementImpl.class)) {
// remove all for loop expression keywords
maybeRemoveKeyword(keywords, Keyword.KW_index);
maybeRemoveKeyword(keywords, Keyword.KW_iterator);
}
if (!parents(IGosuPsiElement.class, GosuStatementListImpl.class) &&
!parents(IGosuPsiElement.class, GosuNotAStatementImpl.class) &&
!inTopLevelProgramExpression()) {
// remove all statement starts
maybeRemoveKeyword(keywords, Keyword.KW_var);
maybeRemoveKeyword(keywords, Keyword.KW_for);
maybeRemoveKeyword(keywords, Keyword.KW_while);
maybeRemoveKeyword(keywords, Keyword.KW_try);
maybeRemoveKeyword(keywords, Keyword.KW_switch);
maybeRemoveKeyword(keywords, Keyword.KW_do);
maybeRemoveKeyword(keywords, Keyword.KW_using);
}
if (!(parents(GosuForEachStatementImpl.class)) &&
!(inProgram() && parents(Object.class, GosuForEachStatementImpl.class))) {
// remove 'in' if we aren't directly in a for loop
maybeRemoveKeyword(keywords, Keyword.KW_in);
}
return makeKeywordProposals(keywords);
}
private void maybeRemoveKeyword(@NotNull List<Keyword> keywords, @NotNull Keyword kw) {
// never remove exact keyword matches, to avoid annoying completion behavior
PrefixMatcher prefixMatcher = getResult().getPrefixMatcher();
if (!prefixMatcher.getPrefix().equals(kw.getName())) {
keywords.remove(kw);
}
}
//hack to detect if we are at the top level of a program, so expr/stmt is ambiguious and
// we should include all keywords
private boolean inTopLevelProgramExpression() {
if (inProgram()) {
IParsedElement pe = ((GosuProgramFileImpl) getContext().getOriginalFile()).getParsedElement();
if (pe != null) {
IGosuClass gosuClass = pe.getGosuClass();
if (gosuClass instanceof IGosuProgram) {
return parents(IGosuPsiElement.class, GosuRawPsiElement.class);
}
}
}
return false;
}
private boolean inProgram() {
return getContext().getOriginalFile() instanceof GosuProgramFileImpl;
}
@NotNull
private List<AdditionalSyntaxCompletionProposal> makeKeywordProposals(@NotNull List<Keyword> keywords) {
ArrayList<AdditionalSyntaxCompletionProposal> proposals = new ArrayList<>();
for (Keyword keyword : keywords) {
if (keyword == Keyword.KW_for || keyword == Keyword.KW_while || keyword == Keyword.KW_using) {
proposals.add(makeKeywordProposal(keyword.getName(), "()", -1));
} else if (keyword == Keyword.KW_in || keyword == Keyword.KW_or || keyword == Keyword.KW_and || keyword == Keyword.KW_not) {
proposals.add(makeKeywordProposal(keyword.getName(), " ", 0));
} else if (keyword == Keyword.KW_try) {
proposals.add(makeKeywordProposal(keyword.getName(), " {}", -1));
} else {
proposals.add(makeKeywordProposal(keyword.getName(), "", 0));
}
}
return proposals;
}
private boolean parents(@NotNull Class... parents) {
PsiElement position = _params.getPosition();
PsiElement parent = position.getParent();
for (Class aClass : parents) {
if (!aClass.isInstance(parent)) {
return false;
}
parent = parent.getParent();
}
return true;
}
private List<Keyword> makeExpressionKeywordList() {
return Lists.newArrayList(
Keyword.KW_and,
Keyword.KW_as,
Keyword.KW_block,
Keyword.KW_break,
Keyword.KW_case,
Keyword.KW_catch,
Keyword.KW_continue,
Keyword.KW_do,
Keyword.KW_else,
Keyword.KW_eval,
Keyword.KW_false,
Keyword.KW_final,
Keyword.KW_finally,
Keyword.KW_for,
Keyword.KW_if,
Keyword.KW_in,
Keyword.KW_index,
Keyword.KW_iterator,
Keyword.KW_new,
Keyword.KW_not,
Keyword.KW_null,
Keyword.KW_or,
Keyword.KW_outer,
Keyword.KW_return,
Keyword.KW_statictypeof,
Keyword.KW_super,
Keyword.KW_switch,
Keyword.KW_this,
Keyword.KW_throw,
Keyword.KW_true,
Keyword.KW_try,
Keyword.KW_typeis,
Keyword.KW_typeof,
Keyword.KW_using,
Keyword.KW_var,
Keyword.KW_where,
Keyword.KW_while);
}
@NotNull
private AdditionalSyntaxCompletionProposal makeKeywordProposal(String name, String trailingText, int offsetForCaret) {
return new AdditionalSyntaxCompletionProposal(name, trailingText, offsetForCaret);
}
private void maybeAddSymbols(@NotNull ISymbolTable transientSymbolTable, int weight) {
Collection<ISymbol> listSymbols = transientSymbolTable.getSymbols().values();
filterUnwantedSymbols(listSymbols);
ISymbol[] symbols = listSymbols.toArray(new ISymbol[listSymbols.size()]);
Arrays.sort(symbols, new Comparator<ISymbol>() {
public int compare(@NotNull ISymbol o1, @NotNull ISymbol o2) {
return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName());
}
});
for (ISymbol symbol : symbols) {
if (_prefixMatcher == null || _prefixMatcher.prefixMatches(symbol.getDisplayName())) {
addCompletion(new SymbolCompletionProposal(symbol, weight));
}
}
}
private void maybeAddBlockCompletion(int weight) {
IType type = getCtxType();
if (type instanceof IBlockType) {
StringBuilder sb = new StringBuilder("\\");
final String[] parameterNames = ((IBlockType) type).getParameterNames();
if (parameterNames.length > 0) {
sb.append(" ");
Joiner.on(", ").appendTo(sb, parameterNames);
sb.append(" ");
}
sb.append("-> ");
RawCompletionProposal element = new RawCompletionProposal(sb.toString());
element.setWeight(weight);
addCompletion(element);
}
}
private void maybeAddEnumFields(int weight) {
final IType type = getCtxType();
if (type instanceof IEnumType && type.isEnum()) {
for (String constant : ((IEnumType) type).getEnumConstants()) {
final RawCompletionProposal proposal = new RawCompletionProposal(constant);
proposal.setWeight(weight);
addCompletion(proposal);
}
}
}
private void filterUnwantedSymbols(@NotNull Collection<ISymbol> listSymbols) {
List<ISymbol> deleteSyms = new ArrayList<>();
for (ISymbol s : listSymbols) {
if (s.getType().getDisplayName().equals(GosuTypes.DEF_CTOR_TYPE().getDisplayName())) {
// Filter constructor symbols (only applicable when editing a gs class)
deleteSyms.add(s);
} else if (s.getName()==null || s.getName().startsWith("@")) {
deleteSyms.add(s);
} else if (_prefixMatcher == null || !_prefixMatcher.prefixMatches(s.getDisplayName())) {
deleteSyms.add(s);
} else if (s instanceof IDynamicFunctionSymbol && ((IDynamicFunctionSymbol) s).isConstructor()) {
// Filter any constructors
deleteSyms.add(s);
}
}
listSymbols.removeAll(deleteSyms);
}
}