Webstormプラグインと自動アドオン

不足している自動アドオンをIDEA IDEファミリに追加する簡単な方法を共有したいと思います。 私たちの場合、WebStormまたはPhpStromに。



プロジェクトの前面にrequire.jsライブラリがあります。 そして、それを使用する場合、特定のファイルへのパスを指定して、それらを追加する必要があります。 残念ながら、これらのファイルへのパスは、手で書くか、部分的にコピーする必要があります。

そして、これを修正し、ファイルへのパスの自動補完を追加する必要があると考えました。





その後、Ideaのプラグインの作成方法に関する情報を探し始め zenden2k habrayuzerがkohanaのリンク解決のためのプラグインの作成方法について話した記事を思い出しました。 私の記事を読む前に、必ず読む必要があります。



リンク解決も非常に便利な機能であると判断したので、最初にこのためのプラグインを作成しました。

プラグインを作成するときに、Idea Community EditionのJavaScriptファイルのPSI構造が不足しているという問題に遭遇しました。これがないと、リンク解決に必要な要素を決定するために必要なJSファイルの構造を決定できませんでした。 Idea Ultimate EAPをインストールする必要がありました。 Idea UTでは、Javascript用のプラグインをインストールする必要があります。その後、PSI Viewer([ツール]-> [PSI構造の表示])で、JavascriptファイルのPSI構造を選択できます。

スクリーンショット
画像



また、この記事の執筆以降、JetBrainsがPHPおよびJSのopenapiを展開したため、特定のPSI JSLiteralExpression要素へのバインドを使用しました。 私のPsiReferenceContributorは次のようになり始めました。

RequirejsPsiReferenceContributor.java
package requirejs; import com.intellij.lang.javascript.psi.JSLiteralExpression; import com.intellij.patterns.StandardPatterns; import com.intellij.psi.PsiReferenceContributor; import com.intellij.psi.PsiReferenceRegistrar; public class RequirejsPsiReferenceContributor extends PsiReferenceContributor { @Override public void registerReferenceProviders(PsiReferenceRegistrar psiReferenceRegistrar) { RequirejsPsiReferenceProvider provider = new RequirejsPsiReferenceProvider(); psiReferenceRegistrar.registerReferenceProvider(StandardPatterns.instanceOf(JSLiteralExpression.class), provider); } }
      
      





ご覧のとおり、PsiElement.classの代わりに、すでにJSLiteralExpression.classを具体的に使用しているため、行内のすべての要素を処理する必要はありません。

ただし、openapiを使用するには、アイデアのあるプラグインプロジェクトで接続する必要があります。 これを行うには、プロジェクト構造に移動し、そこでライブラリを選択します。 中央の列の上にある+をクリックし、Javaを選択して、開いたファイル選択ウィンドウでファイル「/path_to_webstrom/plugins/JavaScriptLanguage/lib/javascript-openapi.jar」を選択します。

スクリーンショット
画像






次に、[モジュール]に移動し、[依存関係]タブを開きます。そこで、javascript-openapiに対して、ScopeをProvidedとして指定します。

スクリーンショット
画像






これらの操作の後、IDEはjavascriptのopenapiに含まれるクラス名およびその他のものを要求します。



また、PsiReferenceProviderを変更してリフレクションから保存する必要がありましたが、次のようになりました。

RequirejsPsiReferenceProvider.java
 package requirejs; import com.intellij.ide.util.PropertiesComponent; import com.intellij.lang.javascript.psi.JSCallExpression; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceProvider; import com.intellij.util.ProcessingContext; import org.jetbrains.annotations.NotNull; public class RequirejsPsiReferenceProvider extends PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { Project project = psiElement.getProject(); PropertiesComponent properties = PropertiesComponent.getInstance(project); String webDirPrefString = properties.getValue("web_dir", "webfront/web"); VirtualFile webDir = project.getBaseDir().findFileByRelativePath(webDirPrefString); if (webDir == null) { return PsiReference.EMPTY_ARRAY; } try { String path = psiElement.getText(); if (isRequireCall(psiElement)) { PsiReference ref = new RequirejsReference(psiElement, new TextRange(1, path.length() - 1), project, webDir); return new PsiReference[] {ref}; } } catch (Exception ignored) {} return new PsiReference[0]; } public static boolean isRequireCall(PsiElement element) { PsiElement prevEl = element.getParent(); if (prevEl != null) { prevEl = prevEl.getParent(); } if (prevEl != null) { if (prevEl instanceof JSCallExpression) { try { if (prevEl.getChildren().length > 1) { if (prevEl.getChildren()[0].getText().toLowerCase().equals("require")) { return true; } } } catch (Exception ignored) {} } } return false; } }
      
      





次に、このリンクを解決するメソッドを実装する必要があります。

そして、ここで私は、アイデアのためのプラグインを書くことについての非常に平均的な情報に接続されたプラグを手に入れました。 事実、最初は「リソースルート」とマークされたディレクトリからファイルの検索を開始したかったのですが、残念ながら、そのようなディレクトリを取得する方法を見つけることができませんでした。 そのため、 zenden2kの記事で説明されているように設定ページを実装した設定からディレクトリへのパスを使用することにしましたので、繰り返しません。

途中でファイルを検索する必要があるディレクトリを見つけた後、すべてが簡単になりました。 VirtualFileクラスには、パス文字列を入力として受け取り、指定されたパスにファイルが存在するかどうかを検索し、存在する場合はVirtualFileクラスのインスタンスとして返すfindFileByRelativePathメソッドがあります。 そのため、PsiElementから文字列値を取得し、余分な部分を切り取り、不足しているものを追加して、そのようなファイルが存在するかどうかを確認する必要がありました。 存在する場合は、PsiElementのインスタンスとして単純にリンクを返します。 resolveメソッドは次のようになります。

RequirejsReverence.java::resolve()
  @Nullable @Override public PsiElement resolve() { String path = element.getText(); path = path.replace("'", "").replace("\"", ""); if (path.startsWith("tpl!")) { path = path.replace("tpl!", ""); } else { path = path.concat(".js"); } if (path.startsWith("./")) { path = path.replaceFirst( ".", element .getContainingFile() .getVirtualFile() .getParent() .getPath() .replace(webDir.getPath(), "") ); } VirtualFile targetFile = webDir.findFileByRelativePath(path); if (targetFile != null) { return PsiManager.getInstance(project).findFile(targetFile); } return null; }
      
      





これを行った後、リンクする許可を得て、オートコンプリートの実装を開始することができました。



アイデアでは、オートコンプリートを実装する2つの方法があります。 最初の簡単な方法は、PsiReferenceインターフェイスのgetVariantsメソッドを実装することで、2番目の高度な方法はCompletionContributorを使用します。 両方の方法を試してみましたが、CompletionContributorには自分自身で利点が見つからなかったため、最初の方法を使用することに決めました。

自動補完の場合、要素のリストを配列の形式で返す必要があります。 文字列、LoookupElementまたはPsiElementの配列にすることができます。

最初は行を返そうとしました。 しかし、それから驚きが待っていました。 事実は、アイデアが行全体の最後のスラッシュの後にスラッシュを含む行を挿入するということです。 さらに、スラッシュの後に値のみを含む文字列を発行した場合、この文字列が自動補完に適しているとは考えていません。 この動作は私には完全には明らかではありません。 また、スラッシュを含む行の自動補完を正しく行う方法に関する情報や、ファイルのパスを含むオプションとしての情報を見つけることができませんでした。

このために私は自分のやり方でやった。

自分で値の挿入を制御するには、InsertHandlerインターフェースを実装し、その中のhandleInsertメソッドで必要なアクションを実行する必要があります。 そして、それを使用するには、文字列だけでなく、InsertHandlerが必要なLookupElementを返す必要があります。

そこで、次のようにLookupElementクラスを拡張しました。

RequirejsLookupElement.java
 package requirejs; import com.intellij.codeInsight.completion.InsertHandler; import com.intellij.codeInsight.completion.InsertionContext; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; public class RequirejsLookupElement extends LookupElement { String path; PsiElement element; private InsertHandler<LookupElement> insertHandler = null; public RequirejsLookupElement(String path, InsertHandler<LookupElement> insertHandler, PsiElement element) { this.path = path; this.insertHandler = insertHandler; this.element = element; } public void handleInsert(InsertionContext context) { if (this.insertHandler != null) { this.insertHandler.handleInsert(context, this); } } @NotNull @Override public String getLookupString() { return path; } }
      
      





InsertHandlerの実装は次のようになります。

RequirejsInsertHandler.java
 package requirejs; import com.intellij.codeInsight.completion.InsertHandler; import com.intellij.codeInsight.completion.InsertionContext; import com.intellij.codeInsight.lookup.LookupElement; public class RequirejsInsertHandler implements InsertHandler { private static final RequirejsInsertHandler instance = new RequirejsInsertHandler(); @Override public void handleInsert(InsertionContext insertionContext, LookupElement lookupElement) { if (lookupElement instanceof RequirejsLookupElement) { insertionContext.getDocument().replaceString( ((RequirejsLookupElement) lookupElement).element.getTextOffset() + 1, insertionContext.getTailOffset(), ((RequirejsLookupElement) lookupElement).path ); } } public static RequirejsInsertHandler getInstance() { return instance; } }
      
      





handleInsertメソッドの本質は、lookupElementを取得して表示および選択されたPsiElementを取得し、PsiElementからファイル内の位置を取得し、行全体をlookupElement.pathのテキストで置き換えることです。 もちろんこれは最善の方法ではありませんが、残念ながら私は別のものを見つけることができませんでした。



その後、一致するすべてのファイルを検索し、LookupElementの配列として返しました。

RequirejsReferenceの完全なリストは次のとおりです。

RequirejsReference.java
 package requirejs; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl; import com.intellij.openapi.vfs.newvfs.impl.VirtualFileImpl; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReference; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; public class RequirejsReference implements PsiReference { PsiElement element; TextRange textRange; Project project; VirtualFile webDir; public RequirejsReference(PsiElement element, TextRange textRange, Project project, VirtualFile webDir) { this.element = element; this.textRange = textRange; this.project = project; this.webDir = webDir; } @Override public PsiElement getElement() { return this.element; } @Nullable @Override public PsiElement resolve() { String path = element.getText(); path = path.replace("'", "").replace("\"", ""); if (path.startsWith("tpl!")) { path = path.replace("tpl!", ""); } else { path = path.concat(".js"); } if (path.startsWith("./")) { path = path.replaceFirst( ".", element .getContainingFile() .getVirtualFile() .getParent() .getPath() .replace(webDir.getPath(), "") ); } VirtualFile targetFile = webDir.findFileByRelativePath(path); if (targetFile != null) { return PsiManager.getInstance(project).findFile(targetFile); } return null; } @Override public String toString() { return getCanonicalText(); } @Override public boolean isSoft() { return false; } @NotNull @Override public Object[] getVariants() { ArrayList<String> files = filterFiles(this.element); ArrayList<LookupElement> completionResultSet = new ArrayList<LookupElement>(); for (int i = 0; i < files.size(); i++) { completionResultSet.add( new RequirejsLookupElement( files.get(i), RequirejsInsertHandler.getInstance(), this.element ) ); } return completionResultSet.toArray(); } protected ArrayList<String> getAllFilesInDirectory(VirtualFile directory) { ArrayList<String> files = new ArrayList<String>(); VirtualFile[] childrens = directory.getChildren(); if (childrens.length != 0) { for (int i = 0; i < childrens.length; i++) { if (childrens[i] instanceof VirtualDirectoryImpl) { files.addAll(getAllFilesInDirectory(childrens[i])); } else if (childrens[i] instanceof VirtualFileImpl) { files.add(childrens[i].getPath().replace(webDir.getPath() + "/", "")); } } } return files; } protected ArrayList<String> filterFiles (PsiElement element) { String value = element.getText().replace("'", "").replace("\"", "").replace("IntellijIdeaRulezzz ", ""); Boolean tpl = value.startsWith("tpl!"); String valuePath = value.replaceFirst("tpl!", ""); ArrayList<String> allFiles = getAllFilesInDirectory(webDir); ArrayList<String> trueFiles = new ArrayList<String>(); String file; for (int i = 0; i < allFiles.size(); i++) { file = allFiles.get(i); if (file.startsWith(valuePath)) { if (tpl && file.endsWith(".html")) { trueFiles.add("tpl!" + file); } else if (file.endsWith(".js")) { trueFiles.add(file.replace(".js", "")); } } } return trueFiles; } @Override public boolean isReferenceTo(PsiElement psiElement) { return false; } @Override public PsiElement bindToElement(@NotNull PsiElement psiElement) throws IncorrectOperationException { throw new IncorrectOperationException(); } @Override public PsiElement handleElementRename(String s) throws IncorrectOperationException { throw new IncorrectOperationException(); } @Override public TextRange getRangeInElement() { return textRange; } @NotNull @Override public String getCanonicalText() { return element.getText(); } }
      
      





ファイル検索方法は再帰的であるため個別に強調表示し、テンプレートにはhtmlのみが必要であり、残りにはjsファイルが必要なので、ファイルフィルタリング方法も強調表示しました。 また、貼り付けるとき、テンプレートはtpl!、プレフィックスで挿入され、jsファイルはjs拡張子なしで挿入されます。



UPD

コメントでは、 VISTALLのユーザーは、LookupElementの下位クラスに独自のクラスを作成する必要はないと示唆しました。 代わりに、LookupElementBuilderを使用できます。これにより、使用するinsertHandlerと、参照するPsiElementを指定できます。

LookupElementBuilderを使用するために、RequirejsReference :: getVariantsメソッドを次のように変更しました。

RequirejsReference :: getVariants
  @NotNull @Override public Object[] getVariants() { ArrayList<String> files = filterFiles(element); ArrayList<LookupElement> completionResultSet = new ArrayList<LookupElement>(); for (int i = 0; i < files.size(); i++) { completionResultSet.add( LookupElementBuilder .create(element, files.get(i)) .withInsertHandler( RequirejsInsertHandler.getInstance() ) ); } return completionResultSet.toArray(); }
      
      





生成されたLookupElementがどのPsiElementに属しているかを知るには、createメソッドを呼び出して、PsiElementを最初のパラメーターとして渡し、2番目の行を2番目のパラメーターとして自動補完に使用します。

また、RequirejsInsertHandler :: handleInsert自体も次のように変更しました。

RequirejsInsertHandler :: handleInsert
  @Override public void handleInsert(InsertionContext insertionContext, LookupElement lookupElement) { insertionContext.getDocument().replaceString( lookupElement.getPsiElement().getTextOffset() + 1, insertionContext.getTailOffset(), lookupElement.getLookupString() ); }
      
      





それから、lookupElement型チェックを削除し、メソッドを使用してPsiElementと置換文字列を取得しました。

これらの操作の後、RequirejsLookupElementクラスは不要になりました。



UPD 2

プラグインは少し完成し、githubに投稿されます: github.com/Fedott/WebStormRequireJsPlugin

同じプラグインが公式のjetbrainsリポジトリで利用可能になりました: plugins.jetbrains.com/plugin/7337

ウィッシュリストを送信して、GitHubまたはこちらで作成できます。



それだけです

どのように実装するのが最善かについての質問やヒントがあります。それらを読んでうれしいです。



All Articles