📜 ⬆️ ⬇️

Plugin for webstorm and auto-complement

I would like to share a simple way to add the missing auto-addition to the IDEA IDE family. In our case, to WebStorm or PhpStrom.

We have a require.js library on the front of our project. And when working with it, you need to specify the paths to certain files in order to add them depending. Unfortunately, the paths to these files have to be written by hand or copied in parts.
And I thought that this would need to be corrected, and add auto-complement the path to the files.


After that, I began to look for information on how to write plug-ins to Idea and remembered an article about habrayuser zenden2k , in which he told how to make a plugin to resolve links for kohana. Before reading my article, be sure to read it.
')
Having decided that link resolution is also a very useful feature, I first wrote a plugin for this purpose.
When writing the plug-in, I ran into the problem of the lack of a PSI structure for javascript files in Idea Community Edition, and without this it was not possible to determine the structure of the JS file, which is needed to determine the necessary element for link resolution. I had to put myself an Idea Ultimate EAP. In Idea UT, you need to install a plug-in for Javascript, and then in the PSI Viewer (Tools -> View PSI Structure) there will be a choice of PSI structure for Javascript files.
Screenshot
image

Also due to the fact that since the writing of that article, JetBrains have rolled out openapi for PHP and JS, I have used the binding already to a specific PSI element JSLiteralExpression. My PsiReferenceContributor began to look like this:
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); } } 

As you can see, instead of PsiElement.class, I have already used specifically JSLiteralExpression.class, so that I would not have to handle all the elements in a row.
But in order to be able to use openapi you need to connect it in the plugin project in idea. To do this, go to the Project Structure, there select Libraries. Click on + above the central column, select Java and in the file selection window that opens, select the file "/path_to_webstrom/plugins/JavaScriptLanguage/lib/javascript-openapi.jar":
Screenshot
image

Then go to Modules, open the Dependencies tab, and there on the vs javascript-openapi we specify Scope as Provided:
Screenshot
image

After these manipulations, the IDE will prompt the names of classes and other things that go into openapi for javascript.

It was also necessary to change the PsiReferenceProvider, freeing it from reflection, went something like this:
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; } } 

Next, you need to implement a method responsible for allowing this link.
And here I had a plug-in associated with very meager information about writing plug-ins to idea. The fact is that initially I had a desire to start searching for files from directories labeled as “Resource Root”, but alas, I could not find how to get such directories. Therefore, I decided to take the path to the directory from the settings, for which I implemented the settings page as described in the article zenden2k , so I will not repeat.
After we found out the directory in which we need to search for files along the way, everything was simple. The VirtualFile class has a findFileByRelativePath method, which accepts a path string as an input, and searches for a file on the given path, and yes, it returns it as an instance of the VirtualFile class. So it was necessary to take the value of the line from PsiElement, cut out the excess, add the missing and check whether such a file exists. If it exists, then simply return the reference to it as an instance of PsiElement. The resolve method looks like this:
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; } 

By doing this, I received permission from the links and you could start implementing the auto-add-on.

In idea, there are two ways to implement an auto-complement. The first is to implement the PsiReference interface getVariants method, and the second one is to use the CompletionContributor. I tried both methods, but I didn’t find any advantages in the CompletionContributor, so I stopped using the first method.
For auto-add-ons, we need to return the list of elements as an array. This can be an array with strings, LoookupElement or PsiElement.
At the beginning I tried to return the string. But here I was in for a surprise. The fact is that the idea line with slashes inserts after the last slash the entire line. Moreover, if you only output a string with a value after a slash, then idea does not perceive this string as suitable for autocompletion. This behavior is not entirely clear to me. And I didn’t manage to find information on how to make auto-add-ons with slashes or as an option with paths for files.
By this he did his own way.
In order to manage the insertion of a value yourself, you need to implement the InsertHandler interface and perform the necessary actions in the handleInsert method. And to use it, you need to return not just a string, but a LookupElement, which will contain the InsertHandler we need.
So I extended the LookupElement class like this:
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; } } 

The implementation of InsertHandler looks like this:
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; } } 

The essence of the handleInsert method is that we take the lookupElement, get the PsiElement for which it was shown and selected, from PsiElement we get its location in the file and replace it with the text from lookupElement.path the entire length of the element. Of course this is not the best way, but unfortunately I could not find another.

After that, I did a search for all matching files, and returned them in the form of a LookupElement array.
Here is the full RequirejsReference listing:
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(); } } 

I selected the file search method separately, because it is recursive, and I also selected the file filtering method, since templates only need html, and for the rest, js files are needed. Also, when inserting, templates are inserted with the tpl! Prefix! And js files are inserted without the js extension.

UPD :
In the comments, the user VISTALL , suggested that the creation of the own class of the successor LookupElement was superfluous. Instead, you can use a LookupElementBuilder, which allows you to specify which insertHandler to use and which PsiElement it refers to.
To use LookupElementBuilder, I modified the RequirejsReference :: getVariants method as follows:
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(); } 

In order for the generated LookupElement to know which PsiElement it belongs to, it is enough to call the create method, passing the first parameter to PsiElement, and the second line to use for autocompletion.
I also changed RequirejsInsertHandler :: handleInsert myself so:
RequirejsInsertHandler :: handleInsert
  @Override public void handleInsert(InsertionContext insertionContext, LookupElement lookupElement) { insertionContext.getDocument().replaceString( lookupElement.getPsiElement().getTextOffset() + 1, insertionContext.getTailOffset(), lookupElement.getLookupString() ); } 

From it, I removed the check for the lookupElement type and used the methods to get the PsiElement and the replacement string.
After these manipulations, the RequirejsLookupElement class is no longer needed.

UPD 2 :
The plugin is slightly finished and posted on github: github.com/Fedott/WebStormRequireJsPlugin
The same plugin is now available in the jetbrains official repository: plugins.jetbrains.com/plugin/7337
Wishlist can be sent to write on github or here.

That's all.
There are questions or tips on how best to implement, I will be glad to read them.

Source: https://habr.com/ru/post/189168/


All Articles