コード補完をホゲってみる

さらに調子に乗ってコード補完に出てくる候補をいじくってみた.
こんなの


とか,こんなの


参考にしたのは,WicketForgeとDevKitに入ってたStruts Assistant.先のスクリーンショットで言うと,最初のJSPの例がWicketForge風で,もういっこのJavaの例がStruts Assistant風.
使っているAPIからして,Struts Assistantのほうが真っ当な感じはする.ただ,どっちにしても素のPlugin SDKだけでは実装できず,idea.jarを必要とするので,コード補完をホゲるってのは実はアングラ技なの?と思わずにはいられなかった.


まずは,WicketForge風なコード補完のホゲり方.
project-componentとして,こんなコンポーネントを登録する.

public class TestProjectComponent implements ProjectComponent {
  public TestProjectComponent(Project project) { }

  public void initComponent() { }
  public void disposeComponent() { }

  @NotNull
  public String getComponentName() {
     return "TestProjectComponent";
  }

  public void projectOpened() {
    // ファイルタイプ(*.jsp)に独自のCompletionDataを登録する
    FileType jspFileType = FileTypeManager.getInstance().getFileTypeByExtension("jsp");
    CompletionUtil.registerCompletionData(jspFileType, new TestCompletionData());
  }

  public void projectClosed() { }
}

ポイントはprojectOpened()部分.コード補完を実現するコンポーネント:CompletionDataの子どもである,TestCompletionDataを登録する.どうゆう訳か,ファイルタイプが "*.java" だとうまく動いてくれない.
んで,そのTestCompletionDataは,こんなコード.

// 継承する CompletionData もいくつかバリエーションがあるんだけど,
// どれを選ぶべきかは分かってない.:-P
public class TestCompletionData extends HtmlCompletionData {
  public TestCompletionData() {
    try {
      // コード補完のトリガとなるキーワードを指定する("${"がトリガ)
      LeftNeighbour left = new LeftNeighbour(new TextFilter("${"));
      CompletionVariant completionVariant = new CompletionVariant(left);
      // ここら辺はよく分かってない
      completionVariant.includeScopeClass(LeafPsiElement.class, true);
      completionVariant.addCompletionFilterOnElement(TrueFilter.INSTANCE);
      // 候補をあげる KeywordChooser を登録する
      completionVariant.addCompletion(new SimpleKeywordChooser());
      completionVariant.setInsertHandler(new DefaultInsertHandler());
      registerVariant(completionVariant);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // やっつけなキーワードチューザ
  public static class SimpleKeywordChooser implements KeywordChooser {
    public String[] getKeywords(CompletionContext completionContext, PsiElement psiElement) {
      return new String[] { "ほげほげ", "ハゲハゲ" };
    }
  }
}

思ったより簡単.KeywordChooserのgetKeywords()に渡されるPsiElementは,コード補完を発火したカーソル付近のPsiElementなので,それを元手にコンテキストにちなんだ候補を返す事もできる(つか,そう使うのが本来の用途なのだろう).参考までに,WicketForgeのHTMLのコード補完をホゲるJavaIdCompletionDataのサンプルコードを載せる.

  public String[] getKeywords(CompletionContext completionContext, PsiElement psiElement) {
    PsiFile containingFile = psiElement.getContainingFile();
    if (containingFile == null) return EMPTY_KEYWORDS;
    VirtualFile virtualFile = containingFile.getOriginalFile().getVirtualFile();
    if (virtualFile == null || virtualFile.getExtension() == null) return EMPTY_KEYWORDS;

    String fileExtension = virtualFile.getExtension();
    String fileName = virtualFile.getNameWithoutExtension();
    if (!Constants.HTML.equalsIgnoreCase(fileExtension)) return EMPTY_KEYWORDS;

    PsiFile origFile = containingFile.getOriginalFile();
    if (origFile == null) return EMPTY_KEYWORDS;

    PsiDirectory javaDirectory = origFile.getContainingDirectory();
    if (javaDirectory == null) return EMPTY_KEYWORDS;

    PsiFile javaFile = javaDirectory.findFile(fileName + "." + Constants.JAVA);
    if (javaFile == null) return EMPTY_KEYWORDS;

    JavaWicketIdVisitor visitor = new JavaWicketIdVisitor();
    javaFile.accept(visitor);
    return visitor.getReferencesArray();
  }

面白いのが,JavaWicketIdVisitor.PsiRecursiveElementVisitorを継承し,Javaソースコードを走査して,wicket:idの元ネタ(Wicketコンポーネントのコンストラクタの第一引数)をピックアップしてる.あーなるほど,PsiRecursiveElementVisitorってこうゆう風にも使えるんだ.


WicketForge方式の良い点は実装が簡単なこと.悪い点というかなんというか,WicketForge方式はDevKitに付いてるOpenAPIのJavadocに一切載ってないAPIばかり使ってる事.なので先のEAPによっては使えなくなる可能性がある(ちなみに,IDEA7m2で試してる).
つーかよ,そんなUndocumentedなやり方,どうやって見つけてんだろ?

        • -

続いて,Struts Assistant風なやりかた.
こっちはWicketForgeと比べると正規のOpenAPIを使っているので,どっちかとえば真っ当なやり方っぽい.しかし,というか故にというか,その分ちょびっと(?)面倒くさい.


こちらも大元はproject-componentで登録する.

public class TestReferenceProvider implements ProjectComponent {
  private final ReferenceProvidersRegistry registry;

  public TestReferenceProvider(Project project) {
    // OpenAPIにReferenceProvidersRegistryというのがいるらしい
    registry = ReferenceProvidersRegistry.getInstance(project);
  }

  public void initComponent() {
    // 独自ReferenceProviderを登録する
    registry.registerReferenceProvider(
      // PsiMethodCallFilterはStruts Assistantからパクった.
      //    なんとなく想像が付くと思うが,List.add() に対するコード補完をホゲる
      new ParentElementFilter(new PsiMethodCallFilter("java.util.List", "add"), 2),
      //                                           ここは '2'じゃないと有効にならない(↑)
      PsiLiteralExpression.class,
      // これが今回ホゲるReferenceProvider
      new TestReferenceProvidor());
   }

    public void disposeComponent() { }

    @NotNull
    public String getComponentName() {
        return "TestReferenceProvider";
    }

    public void projectOpened() { }
    public void projectClosed() { }
}

このやり方はファイルタイプに貼付けるのではなく,特定のPsiElementに貼付けるみたい(それがたまたま,Javaのコードだったって話か?).


んで,そのReferenceProviderはこんなの.

// ReferenceProviderもいろいろバリエーションがあるんだけど,どれがなにか分かってない.:-D
public class TestReferenceProvidor extends GenericReferenceProvider {
 public PsiReference[] getReferencesByElement(PsiElement psiElement, ReferenceType referenceType) {
   return PsiReference.EMPTY_ARRAY;
 }
 public PsiReference[] getReferencesByString(String s, PsiElement psiElement, ReferenceType referenceType, int i) {
   return PsiReference.EMPTY_ARRAY;
 }

 public PsiReference[] getReferencesByElement(PsiElement psiElement) {
   // Struts Assistantもこのメソッドだけ反応するようにしてあった.
   // なんでかは知らない.
   return new PsiReference[] { new TestReference((PsiLiteralExpression) psiElement) };
 }

 private static class TestReference extends PsiReferenceBase<PsiLiteralExpression> {
   public TestReference(PsiLiteralExpression element) {
     super(element);
   }

   @Nullable
   public PsiElement resolve() {
     // なんだか分かってないけど,null返してもコード補完には影響はなかった
     return null;
   }

   public Object[] getVariants() {
     // 選択可能な候補を返す.
     // 返していい配列の中身は,String/PsiElement/CandidateInfoのいずれか
     return new String[] { "あああ", "いいい" };
   }
 }
}

こちらもコンテキスト依存の候補を作りたい場合は,ReferenceProviderでPsiReference(TestReference)を生成するときに,ネタとなるコンテキスト情報(PsiElement)を渡すなりすればいい.
PsiReferenceが返す候補(getVariants()の戻り値)も文字列:String以外も指定できることから,"Quick Documentation Lookup"や"Quick Definition Lookup"とか出来る要素を候補にもあげられるだと思う.ただ,文字列と異なりPsiElementやCandidateInfoは生成方法におまじないがあるらしく,とたんにハードルが高くなるので,そこまでする熱意が不足する.


あと,Struts Assistant方式だと既存のコード補完を置き換えちゃうみたいね.WicketForge方式だと既存のコード補完と混在するようで.そうゆう点でも,Strtus Assistant方式のほうが,より本格的なのかも(でも,めんどくさい).


こんな具合にコード補完までホゲれると「おおっ」って感じがするねぇ.
まあ,あとは何に使うかだなぁ.:-D


ps.
project-componentとはいえ,どんなプロジェクトでも有効になるので,特定のプロジェクトだけ有効にするのがプラグインの礼儀ってもんかと.
つうわけで,今度はファセット(facet)に手を出してみようかな.