직전까지 제작했던 플러그인은 FactoryMethod를 통해 할당된 객체변수 혹은 MethodChain을 통해 호출된 method, field에서 content assist 기능을 모두 사용할 수 있지만,

MethodChain을 통해 호출된 method, field 에서 ctrl + click을 통한 선언부 이동이 불가능한 문제점이 있었음.

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


이번에는 좀 더 빡셌음.

구글링을 해봐도 eclipse PDT 확장에 관한 문서에 ctrl + click을 통해 선언부로 이동하는 기능을 확장하는 부분에 대한 것은 찾을 수 없었음.

(기껏 있는 건 __call() magic method를 통해 호출되는 '없는 method'의 경우 indexing을 만들어 넣어주는 것 정도)


ctrl + click을 통해 선언부로 이동하는 기능에 대한 이름조차 모르는 상황.

그래서 아예 eclipse pdt 소스코드를 뒤지기로 결심.









extension point


혹시나 싶어서 eclipse pdt의 각 플러그인 디렉토리의 plugin.xml을 열어서 org.eclipse.php.* 로 되어 있는 확장가능한 extension point를 전부 열어서 봤지만 마땅한 부분이 없다는 걸 깨달음.

그래서 다음엔 org.eclipse.php.* 로 되어 있는 extension point가 아니라 eclipse pdt에서 사용하는 extension point를 전부 보기 시작함.

뒤지다가 org.eclipse.php.ui/plugin.xml 에서 Hyperlinnk Detectors 라는 뭔가 그럴듯한 이름을 발견함.

아래 소스코드 처럼 되어 있고, Include 라는 이름도 있는 걸로 봐선, 

이게 ctrl + click 을 통해 선언부로 이동하는 기능이라는 확신이 생김..;

<extension point="org.eclipse.ui.workbench.texteditor.hyperlinkDetectors">
    <hyperlinkDetector
        id="org.eclipse.php.internal.ui.editor.hyperlink.PHPHyperlinkDetector"
        targetId="org.eclipse.php.core.phpsource"
        class="org.eclipse.php.internal.ui.editor.hyperlink.PHPHyperlinkDetector"
        name="%hyperlinkDetector.name">
    </hyperlinkDetector>
    <hyperlinkDetector
        id="org.eclipse.php.internal.ui.editor.hyperlink.IncludeHyperlinkDetector"
        targetId="org.eclipse.php.core.phpsource"
        class="org.eclipse.php.internal.ui.editor.hyperlink.IncludeHyperlinkDetector"
        name="%hyperlinkDetector.name.0">
    </hyperlinkDetector>
</extension>


그래서 실제로 해당 소스들을 까봄.

IncludeHyperlinkDetectorPHPHyperlinkDetector를 찬찬히 뒤져보니,

AbstractHyperlinkDetector를 상속받아 detectorHyperlinks() 라는 method를 구현하고 있음.


detectorHyperlinks()는 매개변수로 아래 3개를 받고 있음.

ITextViewer(뷰어인 듯), IRegion(ctrl+click 하려는 영역), boolean(하이퍼 링크를 여러개 할 것인지 여부?)


그리고 IHyperLink[] 을 반환하는데, ModelElementHyperlink 객체를 생성하여 배열의 첫번째 원소로 넣어서 반환함.

ModelElementHyperlink 객체를 생성할 때 아래 3개의 매개변수가 넘어감.

IRegion: ctrl+click시 hyperlink를 생성할 영역정보. 테스트 해보니 이 데이터만 제대로 있어도 어떻게든 hyperlink 모양은 나옴.

Object: hyperlink를 ctrl+click 했을 때 처리될 정보가 담긴 객체.

OpenAction: hyperlink 클릭 이후 실제 action 정의 가능. 기본 사용시에는 new OpenAction(editor) 형태이지만 override 해서 자체 처리도 가능.


실제 처리되는 로직을 보면 IncludeHyperlinkDetector의 경우엔 2번째 object로 ISourceModule을 넘겨 파일만 열도록 되어 있고,

PHPHyperlinkDetector의 경우엔 좀 더 복잡한 형태로 되어 있으나 결국 ICodeAssist.codeSelect() 에서 반환되는 CodeAssist 객체들을 넘김.


선언부분으로 이동시키려면 PHPHyperlinkDetector에서 하는 형식으로 해야 할텐데,

FactoryMethod의 경우, 기본 정의된 CodeAssist 객체가 아니므로,

ModelElementHyperlink 생성자의 2번째 매개변수는 클릭된 단어와 호출문장을 자체적으로 분석하여 IMethod나 IField 형태로 추출하여 넘겨주면 가능하겠다는 결론 도출.










extension


org.eclipse.php.ui/plugin.xml 에서 PDT가 확장하는 것처럼 org.eclipse.ui.workbench.texteditor.hyperlinkDetectors를 확장.


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




Extension Point filter에 org.eclipse.ui.workbench.texteditor를 입력하고,

하단의 Show only extension points from the required plug-ins 체크해제 한 후,

남은 리스트에서 org.eclipse.ui.workbench.texteditor.hyperlinkDetectors 선택하고 Finish 클릭.




하단 탭 중 plugin.xml 을 선택한 후 추가한 HyperlinkDetector에 관한 부분을 아래 소스코드처럼 입력한 후 저장.

<?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>
    <extension
        point="org.eclipse.ui.workbench.texteditor.hyperlinkDetectors">
        <hyperlinkDetector
            activate="true"
            class="bloodguy.hyperlink.BloodguyHyperlinkDetector"
            id="bloodguy.hyperlink.BloodguyHyperlinkDetector"
            name="BloodguyHyperlinkDetector"
            targetId="org.eclipse.php.core.phpsource">
        </hyperlinkDetector>
    </extension>
</plugin>














package/class


bloodguy.hyperlink 패키지를 추가.


좌측 네비게이션에서 프로젝트 이름을 선택 후 우클릭 > New > Package 선택.




Name에 bloodguy.hyperlink 입력하고 Finish 클릭.




추가한 패키지를 선택한 후 우클릭 > New > Class




Name에 BloodguyHyperlinkDetector를 입력하고 Finish 클릭.




BloodguyHyperlinkDetector 클래스의 소스코드는 아래와 같음.

package bloodguy.hyperlink;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.php.internal.core.PHPVersion;
import org.eclipse.php.internal.core.compiler.ast.parser.ASTUtils;
import org.eclipse.php.internal.core.project.ProjectOptions;
import org.eclipse.php.internal.core.typeinference.PHPClassType;
import org.eclipse.php.internal.core.typeinference.PHPModelUtils;
import org.eclipse.php.internal.core.typeinference.PHPTypeInferenceUtils;
import org.eclipse.php.internal.ui.editor.PHPStructuredEditor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.SourceParserUtil;
import org.eclipse.dltk.internal.core.ModelElement;
import org.eclipse.dltk.internal.ui.editor.EditorUtility;
import org.eclipse.dltk.internal.ui.editor.ModelElementHyperlink;
import org.eclipse.dltk.ti.IContext;
import org.eclipse.dltk.ti.ISourceModuleContext;
import org.eclipse.dltk.ti.types.IEvaluatedType;
import org.eclipse.dltk.ui.actions.OpenAction;

@SuppressWarnings("restriction")
public class BloodguyHyperlinkDetector extends AbstractHyperlinkDetector {
    @Override
    public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
        final PHPStructuredEditor editor = org.eclipse.php.internal.ui.util.EditorUtility.getPHPEditor(textViewer);
        if (editor == null) return null;
        if (region == null) return null;

        IModelElement input = EditorUtility.getEditorInputModelElement(editor, false);
        if (input == null) return null;

        PHPVersion phpVersion = ProjectOptions.getPhpVersion(input.getScriptProject().getProject());
        boolean namespacesSupported = phpVersion.isGreaterThan(PHPVersion.PHP5);
        IDocument doc = textViewer.getDocument();
        final int offset = region.getOffset();

        try {
            IRegion wordRegion = findWord(doc, offset, namespacesSupported);
            if (wordRegion == null) return null;

            String word;
            try {
                // 커서 위치의 단어 추출
                word = doc.get(wordRegion.getOffset(), wordRegion.getLength());
                //System.out.println("word = "+word);
            } catch (BadLocationException e) {
                return null;
            }

            IModelElement[] elements = new ModelElement[]{ null };
            boolean found = false;

            // class 추출
            IRegion lineRegion = doc.getLineInformationOfOffset(offset);
            String line = doc.get(lineRegion.getOffset(), lineRegion.getLength());
            int end = wordRegion.getOffset() - lineRegion.getOffset();
            line = line.substring(0, end);
            String className = this.getClassName(line);
            if (className == null) return null;

            IEvaluatedType evaluatedType = new PHPClassType(className);
            // method, field 리스트를 추출해서 앞서 추출한 단어와 일치하는게 있는지 체크
            ModuleDeclaration moduleDeclaration = SourceParserUtil.getModuleDeclaration((ISourceModule)input, null);
            IContext context = ASTUtils.findContext((ISourceModule)input, moduleDeclaration, offset);
            IType[] types = PHPTypeInferenceUtils.getModelElements(evaluatedType, (ISourceModuleContext)context, offset);

            for (IType type : types) {
                try {
                    // methods
                    IMethod[] methods = PHPModelUtils.getTypeHierarchyMethod(type, null, "", false, null);
                    for (IMethod method : methods) {
                        if (method.getElementName().equals(word)) {
                            //System.out.println("[method] = "+method.toString());
                            elements[0] = (IModelElement)method;
                            found = true;
                            break;
                        }
                    }
                    if (found) break;

                    // fields
                    IField[] fields = PHPModelUtils.getTypeHierarchyField(type, null, "", false, null);
                    for (IField field : fields) {
                        String fieldName = field.getElementName().substring(1).trim();
                        if (word.equals(fieldName)) {
                            //System.out.println("[field] "+fieldName);
                            elements[0] = (IModelElement)field;
                            found = true;
                            break;
                        }
                    }
                    if (found) break;
                } catch (CoreException e) {
                    System.out.print("CoreException = "+e.toString());
                }
            }

            // 추출된 단어와 일치하는 method, field가 없을 경우 중지
            if (elements[0] == null) return null;

            // hyperlink 반환
            final IHyperlink link;
            link = new ModelElementHyperlink(wordRegion, elements[0], new OpenAction(editor));
            return new IHyperlink[]{ link };
        } catch (Exception e) {
            System.out.println("Exception = "+e.toString());
            return null;
        }
    }

    /**
     * document의 offset 위치의 단어 위치지정 IRegion 추출
     * @param document
     * @param offset
     * @param namespacesSupported
     * @return 단어 위치지정 IRegion 반환
     * */
    public static IRegion findWord(IDocument document, int offset, boolean namespacesSupported) {
        int start = -2;
        int end = -1;

        try {
            int pos = offset;
            char c;
            // start
            int rightmostNsSeparator = -1;
            while (pos >= 0) {
                c = document.getChar(pos);
                if (!Character.isJavaIdentifierPart(c) && (!namespacesSupported || c != '\\')) {
                    break;
                }
                if (namespacesSupported && c == '\\' && rightmostNsSeparator == -1) {
                    rightmostNsSeparator = pos;
                }
            --pos;
            }
            start = pos;

            // end
            pos = offset;
            int length = document.getLength();
            while (pos < length) {
                c = document.getChar(pos);
                if (!Character.isJavaIdentifierPart(c) && (!namespacesSupported || c != '\\')) {
                    break;
                }
                if (namespacesSupported && c == '\\') {
                    rightmostNsSeparator = pos;
                }
                ++pos;
            }
            end = pos;
    
            if (rightmostNsSeparator != -1) {
                if (rightmostNsSeparator > offset) {
                    end = rightmostNsSeparator;
                } else {
                    start = rightmostNsSeparator;
                }
            }
        } catch (BadLocationException x) {
        }

        if (start >= -1 && end > -1) {
            if (start == offset && end == offset) {
                return new Region(offset, 0);
            } else if (start == offset) {
                return new Region(start, end - start);
            } else {
                return new Region(start + 1, end - start - 1);
            }
        }
        return null;
    }

    /**
     * 1라인의 PHP statement를 넘겨받아 FactoryMethod가 있을 경우 class 이름을 추출하여 반환
     * @param line
     * @return 추출된 class 이름
     * */
    private String getClassName(String line) {
        Pattern factoryPattern = Pattern.compile("\\$this->getModel[(]['\"](\\w+)['\"]\\s*[)]->$");
        Matcher classNameSearcher = factoryPattern.matcher(line);
        if (!classNameSearcher.find()) return null;

        return "model"+classNameSearcher.group(1);
    }
}










dependency


Dependencies에 필요한 플러그인들을 추가해줘야 함.


좌측 네비게이션에서 plugin.xml 더블클릭하고 하단 탭 중 Dependencies 클릭 후 Add 버튼 클릭.




필터에 org.eclipse.php.ui 입력 후 org.eclipse.php.ui를 선택하고 OK 클릭.




같은 방식으로 아래 플러그인들도 추가.

org.eclipse.dltk.ui

org.eclipse.wst.sse.ui

org.eclipse.core.resources














run


이제 Run 버튼을 눌러 플러그인이 적용된 이클립스를 하나 더 띄워서 아래 소스코드의 주석부분에서 methodTypeA() 부분을 ctrl + click 해서 선언부분으로 이동되면 성공.

<?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')->methodTypeA(); // methodTypeA() 마우스 커서를 놓고 ctrl + click
    }
}













마무리


여기까지 되었으면 자체 FactoryMethod에 대해 eclipse PDT에서 기본 제공되던 content assist는 동일하게 사용이 가능해짐.

이제 이 플러그인을 jar 파일로 export 해서 기존에 사용하던 eclipse PDT 경로의 plugins 디렉토리에 jar 파일을 넣고 사용하면 됨.


혹시 기억이 안난다면, jar 파일 만들기는 Eclipse PDT content assist 확장 2장 마지막 부분에 있음.

http://bloodguy.tistory.com/entry/Eclipse-Eclipse-PDT-content-assist-%ED%99%95%EC%9E%A5-2-FactoryMethod-ContentAssist-Assignment


















Posted by bloodguy
,