직전에 제작했던 플러그인의 경우 FactoryMethod를 통해 할당된 변수에서 파생되는 ContentAssist는 가능하지만,

FactoryMethod에서 MethodChain 형태로 바로 파생되는 ContentAssist는 여전히 불가능하다는 문제점이 있음.

이번 장에서는 이를 해결하려 함.


구글링 결과 이를 위한 extension point는 아래와 같다는 결론.

org.eclipse.php.core.completionContextResolvers

org.eclipse.php.core.completionStrategyFactories









extension point


좌측 네비게이션에서 plugin.xml을 더블클릭하여 선택한 다음, 하단 탭 중 Extensions를 클릭하고 Add 버튼을 누름.




'Extension Point filter'에 org.eclipse.php.core 라고 입력한 다음,

'Show only extension points from the required plug-ins' 체크 해제하고,

org.eclipse.php.core.completionContextResolvers 선택 후 Finish 버튼 클릭.



동일한 과정을 거쳐 org.eclipse.php.core.completionStrategyFactories 도 추가.




하단 탭 중 plugin.xml을 클릭한 후 plugin.xml 내용을 아래 소스처럼 입력하고 저장.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
    <extension
        point="org.eclipse.php.core.goalEvaluatorFactories">
        <factory
            class="bloodguy.evaluator.GoalEvaluatorFactory"
            priority="100">
        </factory>
    </extension>
    <extension
        point="org.eclipse.php.core.completionContextResolvers">
            <resolver class="bloodguy.assist.ContextResolver" />
    </extension>
    <extension
        point="org.eclipse.php.core.completionStrategyFactories">
        <factory class="bloodguy.assist.StrategyFactory" />
    </extension>
</plugin>














package/class


좌측 네비게이션 탭에서 project 이름을 선택한 후 우클릭하여 New > Package 선택하여 새 패키지 생성.





패키지 이름은 plugin.xml 에 입력한 대로 bloodguy.assist로 입력하고 Finish 클릭.





추가된 패키지 이름을 선택한 후 우클릭하고 New > Class 선택.





plugin.xml에 입력한대로 ContextResolver 추가하고 아래의 소스코드를 붙여넣고 저장.

package bloodguy.assist;

import org.eclipse.php.core.codeassist.ICompletionContext;
import org.eclipse.php.core.codeassist.ICompletionContextResolver;
import org.eclipse.php.internal.core.codeassist.contexts.CompletionContextResolver;

@SuppressWarnings("restriction")
public class ContextResolver extends CompletionContextResolver implements ICompletionContextResolver {
    @Override
    public ICompletionContext[] createContexts() {
        return new ICompletionContext[] { new bloodguy.assist.Context() };
    }
}






이제 ContextResolver에서 반환할 Context를 만들 차례.

ContextResolver 클래스를 추가한 것과 동일하게 bloodguy.assist 패키지에 Context 클래스를 추가하고 아래 소스를 붙여넣고 저장.

package bloodguy.assist;

import org.eclipse.dltk.core.CompletionRequestor;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.php.internal.core.codeassist.contexts.ClassObjMemberContext;

@SuppressWarnings("restriction")
public class Context extends ClassObjMemberContext {
    @Override
    public boolean isValid(ISourceModule sourceModule, int offset, CompletionRequestor requestor) {
        if (!super.isValid(sourceModule, offset, requestor)) {
            return false;
        }

        // "->" 에서 ctrl + space가 눌러졌는지 체크
        if (getTriggerType() != Trigger.OBJECT) {
            return false;
        }

        // 넘어온 statement가 자체 factory method 형태의 code assist가 필요한 것인지 체크하여 true/false 반환
        FactoryClassnameResolver factorySearcher = new FactoryClassnameResolver(getStatementText());
        if (factorySearcher.containsFactoryCall()) {
            return true;
        }
        return false;
    }
}



Context 클래스에서 사용하는 FactoryClassnameResolver 추가.

package bloodguy.assist;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.php.internal.core.typeinference.PHPClassType;
import org.eclipse.php.internal.core.util.text.TextSequence;

@SuppressWarnings("restriction")
public class FactoryClassnameResolver {
    /**
     * 클래스 이름
     * */
    private String effectiveClassName = "";
    /**
     * offset
    * */
    private int offset = 0;

    /**
     * constructor
     * 넘겨받은 statement에서 code assist할 클래스 이름 추출
     * @param TextSequence inputString
    * */
    public FactoryClassnameResolver(TextSequence inputString) {
        // 정규식으로 추출
        Pattern factoryPattern = Pattern.compile("\\$this->getModel[(]['\"](\\w+)['\"][)]->$");
        Matcher classNameSearcher = factoryPattern.matcher(inputString);

        // 추출실패시 중지
        if (!classNameSearcher.find()) return;

        // 추출된 문자열
        String className = classNameSearcher.group(1);
        if (className.isEmpty()) return;

        // factory method 규약에 맞게 class 이름 지정 후 offset과 함께 멤버변수에 저장
        this.effectiveClassName = "model"+className;
        this.offset = classNameSearcher.end();
    }

    /**
     * factory call인지 여부 반환
     *
     * @return true or false
     */
    public boolean containsFactoryCall() {
        return !effectiveClassName.isEmpty();
    }

    /**
     * factory call에 의해 호출된 클래스 타입 반환
     *
     * @return PHP class type
     */
    public PHPClassType getClassType() {
        return new PHPClassType(effectiveClassName);
    }

    /**
     * context string에서 factory call 종료위치 반환
     *
     * @return end position of broker call
     */
    public int getOffset() {
        return offset;
    }
}



이제 StrategyFactory 추가.

package bloodguy.assist;

import java.util.LinkedList;
import java.util.List;
import org.eclipse.php.core.codeassist.ICompletionContext;
import org.eclipse.php.core.codeassist.ICompletionStrategy;
import org.eclipse.php.core.codeassist.ICompletionStrategyFactory;

public class StrategyFactory implements ICompletionStrategyFactory {
    @Override
    public ICompletionStrategy[] create(ICompletionContext[] contexts) {
        List<ICompletionStrategy> result = new LinkedList<ICompletionStrategy>();
        for (ICompletionContext context : contexts) {
            if (context.getClass() == bloodguy.assist.Context.class) {
                result.add(new bloodguy.assist.Strategy(context));
            }
        }
        return result.toArray(new ICompletionStrategy[result.size()]);
    }
}



StrategyFactory에서 사용하는 Strategy 추가.

package bloodguy.assist;

import java.util.LinkedList;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.core.CompletionRequestor;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ITypeHierarchy;
import org.eclipse.dltk.internal.core.SourceRange;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.php.core.codeassist.ICompletionContext;
import org.eclipse.php.core.codeassist.ICompletionStrategy;
import org.eclipse.php.internal.core.codeassist.ICompletionReporter;
import org.eclipse.php.internal.core.codeassist.contexts.AbstractCompletionContext;
import org.eclipse.php.internal.core.codeassist.contexts.ClassMemberContext;
import org.eclipse.php.internal.core.codeassist.strategies.ClassMethodsStrategy;
import org.eclipse.php.internal.core.typeinference.PHPModelUtils;
import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities;
import org.eclipse.php.internal.core.util.text.TextSequence;

@SuppressWarnings({ "restriction", "deprecation" })
public class Strategy extends ClassMethodsStrategy implements ICompletionStrategy {
    /**
     * constructor
     * */
    public Strategy(ICompletionContext context) {
        super(context);
    }

    /**
     * CompletionReporter에 content assist할 field, method 데이터 할당
     * @param reporter
     * */
    @Override
    public void apply(ICompletionReporter reporter) throws BadLocationException {
        ICompletionContext context = getContext();
        // "->" 에서 요청된 content assist가 아닐 경우 중지
        if (!(context instanceof ClassMemberContext)) return;

        // code assist에 필요한 데이터 세팅
        Context concreteContext = (Context) context;
        CompletionRequestor requestor = concreteContext.getCompletionRequestor();
        String prefix = concreteContext.getPrefix();
        String suffix = "";
        SourceRange replaceRange = getReplacementRange(concreteContext);
        AbstractCompletionContext aContext = (AbstractCompletionContext) context;
        int offset = aContext.getOffset();
        TextSequence statementText = aContext.getStatementText();
        int triggerEnd = getTriggerEnd(statementText);
        boolean exactName = requestor.isContextInformationMode();

        // 멤버변수 리스트 할당
        IType[] types = ContextParser.getTypesFor(aContext.getSourceModule(), statementText, triggerEnd, offset);
        for (IType type : types) {
            try {
                IField[] oneTypeProperties = PHPModelUtils.getTypeHierarchyField(type, prefix, exactName, null);
                for (IField property : oneTypeProperties) {
                    if (isFiltered(property, type, concreteContext)) continue;
                    reporter.reportField(property, suffix, replaceRange, true);
                }
            } catch (CoreException e) {
                e.printStackTrace();
            }
        }

        // 멤버 method 리스트 할당
        List<IMethod> methods = getMethodsFromTypes(types, prefix, exactName, concreteContext);
        for (IMethod method : methods) {
            reporter.reportMethod(method, suffix, replaceRange);
        }
    }

    /**
     * types에서 추출된 method 리스트 반환
     *
     * @param types
     * @param prefix
     * @param exactName
     * @param concreteContext
     * @return method 리스트 반환
     */
    private List<IMethod> getMethodsFromTypes(IType[] types, String prefix, boolean exactName, Context concreteContext) {
        List<IMethod> result = new LinkedList<IMethod>();
        for (IType type : types) {
            try {
                ITypeHierarchy hierarchy = getCompanion().getSuperTypeHierarchy(type, null);
                IMethod[] methods = PHPModelUtils.getTypeHierarchyMethod(type, hierarchy, prefix, exactName, null);
                for (IMethod method : methods) {
                    if (isFiltered(method, type, concreteContext)) continue;
                    result.add(method);
                }
            } catch (CoreException e) {
                System.out.print(e.toString());
            }
        }
        return result;
    }

    /**
     * 트리거 종료위치
     * @param TextSequence statementText
     * @return 트리거 종료위치 반환
     * */
    private int getTriggerEnd(TextSequence statementText) {
        int triggerEnd = PHPTextSequenceUtilities.readBackwardSpaces(statementText, statementText.length());
        triggerEnd = PHPTextSequenceUtilities.readIdentifierStartIndex(statementText, triggerEnd, true);
        triggerEnd = PHPTextSequenceUtilities.readBackwardSpaces(statementText, triggerEnd);
        return triggerEnd;
    }
}



Strategy에서 사용하는 ContextParser 추가.

package bloodguy.assist;

import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.SourceParserUtil;
import org.eclipse.dltk.ti.IContext;
import org.eclipse.dltk.ti.ISourceModuleContext;
import org.eclipse.dltk.ti.types.IEvaluatedType;
import org.eclipse.php.internal.core.codeassist.CodeAssistUtils;
import org.eclipse.php.internal.core.compiler.ast.parser.ASTUtils;
import org.eclipse.php.internal.core.typeinference.PHPTypeInferenceUtils;
import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities;
import org.eclipse.php.internal.core.util.text.TextSequence;

@SuppressWarnings("restriction")
public class ContextParser extends CodeAssistUtils {
    private static IType[] EMPTY_TYPES = new IType[0];

    /**
     * 클래스 타입 리스트 반환
     * @param ISourceModule sourceModule
     * @param TextSequence statementText
     * @param int endPosition
     * @param int offset
     * @return 클래스 타입 리스트 반환
     * */
    public static IType[] getTypesFor(ISourceModule sourceModule, TextSequence statementText, int endPosition, int offset) {
        endPosition = PHPTextSequenceUtilities.readBackwardSpaces(statementText, endPosition);
        String triggerText = statementText.subSequence(endPosition - 2, endPosition).toString();

        // -> 혹은 :: 만 허용
        if (!triggerText.equals(OBJECT_FUNCTIONS_TRIGGER) && !triggerText.equals("::")) return EMPTY_TYPES;

        // 넘어온 statement가 code assist 가 필요한 것인지 체크
        FactoryClassnameResolver factorySearcher = new FactoryClassnameResolver(statementText);
        if (!factorySearcher.containsFactoryCall()) return EMPTY_TYPES;

        int propertyEndPosition = PHPTextSequenceUtilities.readBackwardSpaces(statementText, endPosition - triggerText.length());
        if (factorySearcher.getOffset() >= propertyEndPosition) return getFactoryType(factorySearcher, sourceModule, offset);

        return EMPTY_TYPES;
    }

    /**
     * 클래스 타입 리스트 반환
     * @param FactoryClassnameResolver brokerSearcher
     * @param ISourceModule sourceModule
     * @param int offset
     * @return 클래스 타입 리스트 반환
     * */
    private static IType[] getFactoryType(FactoryClassnameResolver brokerSearcher, ISourceModule sourceModule, int offset) {
        IEvaluatedType evaluatedType = brokerSearcher.getClassType();
        ModuleDeclaration moduleDeclaration = SourceParserUtil.getModuleDeclaration(sourceModule, null);
        IContext context = ASTUtils.findContext(sourceModule, moduleDeclaration, offset);
        return PHPTypeInferenceUtils.getModelElements(evaluatedType, (ISourceModuleContext) context, offset);
    }
}










dependency


추가한 소스코드에서 사용되는 플러그인을 Dependencies에 추가.

좌측 네비게이션에서 plugin.xml을 더블클릭한 다음 나오는 창에서 하단 탭의 Dependencies 클릭하고 Add 버튼 클릭.




필터창에 org.eclipse.jface.text 까지 입력하고 남은 리스트에서 org.eclipse.jface.text 선택 후 OK 클릭.














run


ctrl + s 눌러서 저장 후 Run 버튼을 눌러 현재 플러그인이 적용된 이클립스를 실행시킨 후 아래 소스 코드의 주석으로 표기된 부분에서 content assist가 나오면 성공.

<?PHP
// 모델 A
class modelTypeA
{
    public $typeA;
    public function methodTypeA()
    {}
}

// 모델 B
class modelTypeB
{
    public $typeB;
    public function methodTypeB()
    {}
}

// getModel() 이라는 factory method가 있음
class ParentClass
{
    // 'model' 이란 문자열이 생략된 채로 넘겨받은 이름을 조합하여 적절한 객체를 반환하는 factory method
    public function getModel($modelName)
    {
        $className = 'model'.$modelName;
        if (class_exists($className) === true) return new $className;
        else return null;
    }
}

class ChildClass extends ParentClass
{
    public function getCount()
    {
        return $this->getModel('TypeA')-> // 여기서 ctrl + space를 눌러도 content assist가 나타나지 않음
    }
}














문제점


여기까지 하면 FactoryMethod로 할당된 혹은 MethodChain 형태의 content assist가 모두 가능하지만,

MethodChain 형태로 호출된 method나 field에서 ctrl + click 으로 선언부 이동이 안됨.

return $this->getModel('TypeA')->methodTypeA(); // methodTypeA() 에 ctrl + click 안됨


이 문제의 해결은 다음 장에서.













Posted by bloodguy
,