org.eclipse.flux.jdt.services.CompletionProposalReplacementProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.flux.jdt.services.CompletionProposalReplacementProvider.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Pivotal Software, Inc. and others.
 * All rights reserved. This program and the accompanying materials are made 
 * available under the terms of the Eclipse Public License v1.0 
 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
 * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
 *
 * Contributors:
 *     Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.flux.jdt.services;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.CompletionContext;
import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;

/**
 * Utility to calculate the completion replacement string based on JDT Core
 * {@link CompletionProposal}. This class is based on the implementation of JDT
 * UI <code>AbstractJavaCompletionProposal</code> and its subclasses.
 * 
 * @author aboyko
 *
 */
public class CompletionProposalReplacementProvider {

    final private static char SPACE = ' ';
    final private static char LPAREN = '(';
    final private static char RPAREN = ')';
    final private static char SEMICOLON = ';';
    final private static char COMMA = ',';

    private ICompilationUnit compilationUnit;
    private int offset;
    private String prefix;
    private CompletionProposal proposal;
    private CompletionContext context;
    private ImportRewrite importRewrite;

    public CompletionProposalReplacementProvider(ICompilationUnit compilationUnit, CompletionProposal proposal,
            CompletionContext context, int offset, String prefix) {
        super();
        this.compilationUnit = compilationUnit;
        this.offset = offset;
        this.prefix = prefix;
        this.proposal = proposal;
        this.context = context;
        this.importRewrite = TypeProposalUtils.createImportRewrite(compilationUnit);
    }

    public ProposalReplcamentInfo createReplacement() {
        ProposalReplcamentInfo replacementInfo = new ProposalReplcamentInfo();
        replacementInfo.replacement = createReplacement(proposal, (char) 0, replacementInfo.positions).toString();
        try {
            replacementInfo.extraChanges = importRewrite.rewriteImports(new NullProgressMonitor());
        } catch (CoreException e) {
            e.printStackTrace();
        }
        return replacementInfo;
    }

    public StringBuilder createReplacement(CompletionProposal proposal, char trigger, List<Integer> positions) {
        StringBuilder completionBuffer = new StringBuilder();
        if (isSupportingRequiredProposals(proposal)) {
            CompletionProposal[] requiredProposals = proposal.getRequiredProposals();
            for (int i = 0; requiredProposals != null && i < requiredProposals.length; i++) {
                if (requiredProposals[i].getKind() == CompletionProposal.TYPE_REF) {
                    appendRequiredType(completionBuffer, requiredProposals[i], trigger, positions);
                } else if (requiredProposals[i].getKind() == CompletionProposal.TYPE_IMPORT) {
                    appendImportProposal(completionBuffer, requiredProposals[i], proposal.getKind());
                } else if (requiredProposals[i].getKind() == CompletionProposal.METHOD_IMPORT) {
                    appendImportProposal(completionBuffer, requiredProposals[i], proposal.getKind());
                } else if (requiredProposals[i].getKind() == CompletionProposal.FIELD_IMPORT) {
                    appendImportProposal(completionBuffer, requiredProposals[i], proposal.getKind());
                } else {
                    /*
                     * In 3.3 we only support the above required proposals, see
                     * CompletionProposal#getRequiredProposals()
                     */
                    Assert.isTrue(false);
                }
            }
        }

        //         boolean isSmartTrigger= isSmartTrigger(trigger);

        appendReplacementString(completionBuffer, proposal, positions);

        //         String replacement;
        //         if (isSmartTrigger || trigger == (char) 0) {
        //            int referenceOffset= offset - prefix.length() + completionBuffer.length();
        //            //add ; to the replacement string if replacement string do not end with a semicolon and the document do not already have a ; at the reference offset.
        //            if (trigger == ';'
        //               && proposal.getCompletion()[proposal.getCompletion().length - 1] != ';'
        //               && (referenceOffset >= compilationUnit.getBuffer()
        //                     .getLength() || compilationUnit.getBuffer()
        //                     .getChar(referenceOffset) != ';')) {
        //               completionBuffer.append(';');
        //            }
        //         } else {
        //            StringBuffer buffer= new StringBuffer(getReplacementString());
        //
        //            // fix for PR #5533. Assumes that no eating takes place.
        //            if ((getCursorPosition() > 0 && getCursorPosition() <= buffer.length() && buffer.charAt(getCursorPosition() - 1) != trigger)) {
        //               // insert trigger ';' for methods with parameter at the end of the replacement string and not at the cursor position.
        //               int length= getReplacementString().length();
        //               if (trigger == ';' && getCursorPosition() != length) {
        //                  if (buffer.charAt(length - 1) != trigger) {
        //                     buffer.insert(length, trigger);
        //                  }
        //               } else {
        //                  buffer.insert(getCursorPosition(), trigger);
        //                  setCursorPosition(getCursorPosition() + 1);
        //               }
        //            }
        //
        //            replacement= buffer.toString();
        //            setReplacementString(replacement);
        //         }
        //
        //         // PR 47097
        //         if (isSmartTrigger) {
        //            // avoid inserting redundant semicolon when smart insert is enabled.
        //            if (!(trigger == ';' && (completionBuffer.charAt(completionBuffer.length() - 1) == ';' /*|| document.getChar(referenceOffset) == ';'*/))) { //$NON-NLS-1$
        //               handleSmartTrigger(trigger, offset - prefix.length() + completionBuffer.length());
        //            }
        //         }

        return completionBuffer;
    }

    private boolean isSupportingRequiredProposals(CompletionProposal proposal) {
        return proposal != null && (proposal.getKind() == CompletionProposal.METHOD_REF
                || proposal.getKind() == CompletionProposal.FIELD_REF
                || proposal.getKind() == CompletionProposal.TYPE_REF
                || proposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION
                || proposal.getKind() == CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION);
    }

    protected boolean hasArgumentList(CompletionProposal proposal) {
        if (CompletionProposal.METHOD_NAME_REFERENCE == proposal.getKind())
            return false;
        char[] completion = proposal.getCompletion();
        return !isInJavadoc() && completion.length > 0 && completion[completion.length - 1] == ')';
    }

    private boolean isInJavadoc() {
        return context.isInJavadoc();
    }

    private void appendReplacementString(StringBuilder buffer, CompletionProposal proposal,
            List<Integer> positions) {
        if (!hasArgumentList(proposal)) {
            buffer.append(
                    proposal.getKind() == CompletionProposal.TYPE_REF ? computeJavaTypeReplacementString(proposal)
                            : String.valueOf(proposal.getCompletion()));
            return;
        }

        // we're inserting a method plus the argument list - respect formatter preferences
        appendMethodNameReplacement(buffer, proposal);

        if (hasParameters(proposal)) {
            appendGuessingCompletion(buffer, proposal, positions);
        }

        buffer.append(RPAREN);

        if (canAutomaticallyAppendSemicolon(proposal))
            buffer.append(SEMICOLON);
    }

    private boolean hasParameters(CompletionProposal proposal) throws IllegalArgumentException {
        return Signature.getParameterCount(proposal.getSignature()) > 0;
    }

    private void appendMethodNameReplacement(StringBuilder buffer, CompletionProposal proposal) {
        if (proposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER) {
            String coreCompletion = String.valueOf(proposal.getCompletion());
            //         String lineDelimiter = TextUtilities.getDefaultLineDelimiter(getTextViewer().getDocument());
            //         String replacement= CodeFormatterUtil.format(CodeFormatter.K_EXPRESSION, coreCompletion, 0, lineDelimiter, fInvocationContext.getProject());
            //         buffer.append(replacement.substring(0, replacement.lastIndexOf('.') + 1));
            buffer.append(coreCompletion);
        }

        if (proposal.getKind() != CompletionProposal.CONSTRUCTOR_INVOCATION)
            buffer.append(proposal.getName());

        buffer.append(LPAREN);
    }

    private void appendGuessingCompletion(StringBuilder buffer, CompletionProposal proposal,
            List<Integer> positions) {
        char[][] parameterNames = proposal.findParameterNames(null);

        int count = parameterNames.length;

        for (int i = 0; i < count; i++) {
            if (i != 0) {
                buffer.append(COMMA);
                buffer.append(SPACE);
            }

            char[] argument = parameterNames[i];

            positions.add(buffer.length());
            positions.add(argument.length);

            buffer.append(argument);
        }
    }

    private final boolean canAutomaticallyAppendSemicolon(CompletionProposal proposal) {
        return !proposal.isConstructor() && CharOperation.equals(new char[] { Signature.C_VOID },
                Signature.getReturnType(proposal.getSignature()));
    }

    private StringBuilder appendRequiredType(StringBuilder buffer, CompletionProposal typeProposal, char trigger,
            List<Integer> positions) {

        appendReplacementString(buffer, typeProposal, positions);

        if (compilationUnit == null /*|| getContext() != null && getContext().isInJavadoc()*/) {
            return buffer;
        }

        IJavaProject project = compilationUnit.getJavaProject();
        if (!shouldProposeGenerics(project))
            return buffer;

        char[] completion = typeProposal.getCompletion();
        // don't add parameters for import-completions nor for proposals with an empty completion (e.g. inside the type argument list)
        if (completion.length > 0
                && (completion[completion.length - 1] == ';' || completion[completion.length - 1] == '.'))
            return buffer;

        /*
         * Add parameter types
         */
        boolean onlyAppendArguments;
        try {
            onlyAppendArguments = proposal.getCompletion().length == 0 && offset > 0
                    && compilationUnit.getBuffer().getChar(offset - 1) == '<';
        } catch (JavaModelException e) {
            onlyAppendArguments = false;
        }
        if (onlyAppendArguments || shouldAppendArguments(typeProposal, trigger)) {
            appendParameterList(buffer, computeTypeArgumentProposals(typeProposal), positions, onlyAppendArguments);
        }
        return buffer;
    }

    private final boolean shouldProposeGenerics(IJavaProject project) {
        String sourceVersion;
        if (project != null)
            sourceVersion = project.getOption(JavaCore.COMPILER_SOURCE, true);
        else
            sourceVersion = JavaCore.getOption(JavaCore.COMPILER_SOURCE);

        return !isVersionLessThan(sourceVersion, JavaCore.VERSION_1_5);
    }

    public static boolean isVersionLessThan(String version1, String version2) {
        if (JavaCore.VERSION_CLDC_1_1.equals(version1)) {
            version1 = JavaCore.VERSION_1_1 + 'a';
        }
        if (JavaCore.VERSION_CLDC_1_1.equals(version2)) {
            version2 = JavaCore.VERSION_1_1 + 'a';
        }
        return version1.compareTo(version2) < 0;
    }

    private IJavaElement resolveJavaElement(IJavaProject project, CompletionProposal proposal)
            throws JavaModelException {
        char[] signature = proposal.getSignature();
        String typeName = SignatureUtil.stripSignatureToFQN(String.valueOf(signature));
        return project.findType(typeName);
    }

    private String[] computeTypeArgumentProposals(CompletionProposal proposal) {
        try {
            IType type = (IType) resolveJavaElement(compilationUnit.getJavaProject(), proposal);
            if (type == null)
                return new String[0];

            ITypeParameter[] parameters = type.getTypeParameters();
            if (parameters.length == 0)
                return new String[0];

            String[] arguments = new String[parameters.length];

            ITypeBinding expectedTypeBinding = getExpectedTypeForGenericParameters();
            if (expectedTypeBinding != null && expectedTypeBinding.isParameterizedType()) {
                // in this case, the type arguments we propose need to be compatible
                // with the corresponding type parameters to declared type

                IType expectedType = (IType) expectedTypeBinding.getJavaElement();

                IType[] path = TypeProposalUtils.computeInheritancePath(type, expectedType);
                if (path == null)
                    // proposed type does not inherit from expected type
                    // the user might be looking for an inner type of proposed type
                    // to instantiate -> do not add any type arguments
                    return new String[0];

                int[] indices = new int[parameters.length];
                for (int paramIdx = 0; paramIdx < parameters.length; paramIdx++) {
                    indices[paramIdx] = TypeProposalUtils.mapTypeParameterIndex(path, path.length - 1, paramIdx);
                }

                // for type arguments that are mapped through to the expected type's
                // parameters, take the arguments of the expected type
                ITypeBinding[] typeArguments = expectedTypeBinding.getTypeArguments();
                for (int paramIdx = 0; paramIdx < parameters.length; paramIdx++) {
                    if (indices[paramIdx] != -1) {
                        // type argument is mapped through
                        ITypeBinding binding = typeArguments[indices[paramIdx]];
                        arguments[paramIdx] = computeTypeProposal(binding, parameters[paramIdx]);
                    }
                }
            }

            // for type arguments that are not mapped through to the expected type,
            // take the lower bound of the type parameter
            for (int i = 0; i < arguments.length; i++) {
                if (arguments[i] == null) {
                    arguments[i] = computeTypeProposal(parameters[i]);
                }
            }
            return arguments;
        } catch (JavaModelException e) {
            return new String[0];
        }
    }

    private String computeTypeProposal(ITypeParameter parameter) throws JavaModelException {
        String[] bounds = parameter.getBounds();
        String elementName = parameter.getElementName();
        if (bounds.length == 1 && !"java.lang.Object".equals(bounds[0])) //$NON-NLS-1$
            return Signature.getSimpleName(bounds[0]);
        else
            return elementName;
    }

    private String computeTypeProposal(ITypeBinding binding, ITypeParameter parameter) throws JavaModelException {
        final String name = TypeProposalUtils.getTypeQualifiedName(binding);
        if (binding.isWildcardType()) {

            if (binding.isUpperbound()) {
                // replace the wildcard ? with the type parameter name to get "E extends Bound" instead of "? extends Bound"
                //            String contextName= name.replaceFirst("\\?", parameter.getElementName()); //$NON-NLS-1$
                // upper bound - the upper bound is the bound itself
                return binding.getBound().getName();
            }

            // no or upper bound - use the type parameter of the inserted type, as it may be more
            // restrictive (eg. List<?> list= new SerializableList<Serializable>())
            return computeTypeProposal(parameter);
        }

        // not a wildcard but a type or type variable - this is unambigously the right thing to insert
        return name;
    }

    private StringBuilder appendParameterList(StringBuilder buffer, String[] typeArguments, List<Integer> positions,
            boolean onlyAppendArguments) {
        if (typeArguments != null && typeArguments.length > 0) {
            final char LESS = '<';
            final char GREATER = '>';
            if (!onlyAppendArguments) {
                buffer.append(LESS);
            }
            StringBuffer separator = new StringBuffer(3);
            separator.append(COMMA);

            for (int i = 0; i != typeArguments.length; i++) {
                if (i != 0)
                    buffer.append(separator);

                positions.add(buffer.length());
                positions.add(typeArguments[i].length());
                buffer.append(typeArguments[i]);
            }

            if (!onlyAppendArguments)
                buffer.append(GREATER);
        }
        return buffer;
    }

    private boolean shouldAppendArguments(CompletionProposal proposal, char trigger) {
        /*
         * No argument list if there were any special triggers (for example a
         * period to qualify an inner type).
         */
        if (trigger != '\0' && trigger != '<' && trigger != '(')
            return false;

        /*
         * No argument list if the completion is empty (already within the
         * argument list).
         */
        char[] completion = proposal.getCompletion();
        if (completion.length == 0)
            return false;

        /*
         * No argument list if there already is a generic signature behind the
         * name.
         */
        int index = prefix.length() - 1;
        while (index >= 0 && Character.isUnicodeIdentifierPart(prefix.charAt(index))
                && prefix.charAt(index) != '\n')
            --index;

        if (index < 0)
            return true;

        char ch = prefix.charAt(index);
        return ch != '<' && ch != '\n';

    }

    private StringBuilder appendImportProposal(StringBuilder buffer, CompletionProposal proposal, int coreKind) {
        int proposalKind = proposal.getKind();
        String qualifiedTypeName = null;
        char[] qualifiedType = null;
        if (proposalKind == CompletionProposal.TYPE_IMPORT) {
            qualifiedType = proposal.getSignature();
            qualifiedTypeName = String.valueOf(Signature.toCharArray(qualifiedType));
        } else if (proposalKind == CompletionProposal.METHOD_IMPORT
                || proposalKind == CompletionProposal.FIELD_IMPORT) {
            qualifiedType = Signature.getTypeErasure(proposal.getDeclarationSignature());
            qualifiedTypeName = String.valueOf(Signature.toCharArray(qualifiedType));
        } else {
            /*
             * In 3.3 we only support the above import proposals, see
             * CompletionProposal#getRequiredProposals()
             */
            Assert.isTrue(false);
        }

        /* Add imports if the preference is on. */
        if (importRewrite != null) {
            if (proposalKind == CompletionProposal.TYPE_IMPORT) {
                String simpleType = importRewrite.addImport(qualifiedTypeName, null);
                if (coreKind == CompletionProposal.METHOD_REF) {
                    buffer.append(simpleType);
                    buffer.append(',');
                    return buffer;
                }
            } else {
                String res = importRewrite.addStaticImport(qualifiedTypeName, String.valueOf(proposal.getName()),
                        proposalKind == CompletionProposal.FIELD_IMPORT, null);
                int dot = res.lastIndexOf('.');
                if (dot != -1) {
                    buffer.append(importRewrite.addImport(res.substring(0, dot), null));
                    buffer.append('.');
                    return buffer;
                }
            }
            return buffer; //$NON-NLS-1$
        }

        // Case where we don't have an import rewrite (see allowAddingImports)

        if (compilationUnit != null
                && isImplicitImport(Signature.getQualifier(qualifiedTypeName), compilationUnit)) {
            /* No imports for implicit imports. */

            if (proposal.getKind() == CompletionProposal.TYPE_IMPORT && coreKind == CompletionProposal.FIELD_REF)
                return buffer; //$NON-NLS-1$
            qualifiedTypeName = String.valueOf(Signature.getSignatureSimpleName(qualifiedType));
        }
        buffer.append(qualifiedTypeName);
        buffer.append('.');
        return buffer;
    }

    private static boolean isImplicitImport(String qualifier, ICompilationUnit cu) {
        if ("java.lang".equals(qualifier)) { //$NON-NLS-1$
            return true;
        }
        String packageName = cu.getParent().getElementName();
        if (qualifier.equals(packageName)) {
            return true;
        }
        String typeName = JavaCore.removeJavaLikeExtension(cu.getElementName());
        String mainTypeName = concatenateName(packageName, typeName);
        return qualifier.equals(mainTypeName);
    }

    private static String concatenateName(String name1, String name2) {
        StringBuffer buf = new StringBuffer();
        if (name1 != null && name1.length() > 0) {
            buf.append(name1);
        }
        if (name2 != null && name2.length() > 0) {
            if (buf.length() > 0) {
                buf.append('.');
            }
            buf.append(name2);
        }
        return buf.toString();
    }

    private ITypeBinding getExpectedTypeForGenericParameters() {
        char[][] chKeys = context.getExpectedTypesKeys();
        if (chKeys == null || chKeys.length == 0)
            return null;

        String[] keys = new String[chKeys.length];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = String.valueOf(chKeys[0]);
        }

        final ASTParser parser = ASTParser.newParser(AST.JLS8);
        parser.setProject(compilationUnit.getJavaProject());
        parser.setResolveBindings(true);
        parser.setStatementsRecovery(true);

        final Map<String, IBinding> bindings = new HashMap<String, IBinding>();
        ASTRequestor requestor = new ASTRequestor() {
            @Override
            public void acceptBinding(String bindingKey, IBinding binding) {
                bindings.put(bindingKey, binding);
            }
        };
        parser.createASTs(new ICompilationUnit[0], keys, requestor, null);

        if (bindings.size() > 0)
            return (ITypeBinding) bindings.get(keys[0]);

        return null;
    }

    private String computeJavaTypeReplacementString(CompletionProposal proposal) {
        String replacement = String.valueOf(proposal.getCompletion());

        /* No import rewriting ever from within the import section. */
        if (isImportCompletion(proposal))
            return replacement;

        /*
         * Always use the simple name for non-formal javadoc references to
         * types.
         */
        // TODO fix
        if (proposal.getKind() == CompletionProposal.TYPE_REF && context.isInJavadocText())
            return SignatureUtil.getSimpleTypeName(proposal);

        String qualifiedTypeName = SignatureUtil.getQualifiedTypeName(proposal);

        // Type in package info must be fully qualified.
        if (compilationUnit != null && TypeProposalUtils.isPackageInfo(compilationUnit))
            return qualifiedTypeName;

        if (qualifiedTypeName.indexOf('.') == -1 && replacement.length() > 0)
            // default package - no imports needed
            return qualifiedTypeName;

        /*
         * If the user types in the qualification, don't force import rewriting
         * on him - insert the qualified name.
         */
        int dotIndex = prefix.lastIndexOf('.');
        // match up to the last dot in order to make higher level matching still
        // work (camel case...)
        if (dotIndex != -1
                && qualifiedTypeName.toLowerCase().startsWith(prefix.substring(0, dotIndex + 1).toLowerCase())) {
            return qualifiedTypeName;
        }

        /*
         * The replacement does not contain a qualification (e.g. an inner type
         * qualified by its parent) - use the replacement directly.
         */
        if (replacement.indexOf('.') == -1) {
            if (isInJavadoc())
                return SignatureUtil.getSimpleTypeName(proposal); // don't use
            // the
            // braces
            // added for
            // javadoc
            // link
            // proposals
            return replacement;
        }

        /* Add imports if the preference is on. */
        if (importRewrite != null) {
            return importRewrite.addImport(qualifiedTypeName, null);
        }

        // fall back for the case we don't have an import rewrite (see
        // allowAddingImports)

        /* No imports for implicit imports. */
        if (compilationUnit != null
                && TypeProposalUtils.isImplicitImport(Signature.getQualifier(qualifiedTypeName), compilationUnit)) {
            return Signature.getSimpleName(qualifiedTypeName);
        }

        /* Default: use the fully qualified type name. */
        return qualifiedTypeName;
    }

    private boolean isImportCompletion(CompletionProposal proposal) {
        char[] completion = proposal.getCompletion();
        if (completion.length == 0)
            return false;

        char last = completion[completion.length - 1];
        /*
         * Proposals end in a semicolon when completing types in normal imports
         * or when completing static members, in a period when completing types
         * in static imports.
         */
        return last == ';' || last == '.';
    }

    //   private boolean isSmartTrigger(char trigger) {
    //      return false;
    //   }
    //
    //   private void handleSmartTrigger(char trigger, int refrenceOffset) {
    //
    //   }

}