📜 ⬆️ ⬇️

Creating a link resolution plugin for PhpStorm (IntelliJ IDEA)

I work as a web programmer, write in PHP and use the Kohana framework. For development I use the amazing, in my opinion, PhpStorm environment.

When working with large and not very projects, I was always depressed that I spent a lot of time navigating the project, searching for a particular file (controller or template) in the project tree. Ctrl + Shift + N, unfortunately, is not always convenient.

To begin with, I wanted to make it so that you could go from the controller file by pressing Ctrl + B (or Ctrl + Click) above the name of the template passed to the Cohanov View :: factory () directly into the template file:
')


So I decided to write a small plugin for PhpStorm that would facilitate my work and free me from some part of the routine.



Environment preparation


We will need:
- IntelliJ IDEA Community Edition or Ultimate.
- JDK (you need to download the version from which PhpStorm is built, otherwise the plugin will not start, in my case it was Java 1.6);

Since the documentation for creating IDEA plug-ins is very scarce, it is also recommended to get a copy of Intellij IDEA source codes , and use it as visual documentation :)

Tool setting:


You must configure the Java SDK and IntelliJ IDEA Plugin SDK:
- we start IntelliJ IDEA
- open the menu item File | Project Structure
- select the SDKs tab, click on the plus sign and choose the path to the JDK

- select the Project tab
- click on new, then IntelliJ IDEA Plugin SDK and in the menu that opens - choose the path to PhpStorm (you can go to IntelliJ IDEA, but then we will not be able to debug the plugin in PhpStorm)


You also need to create a Run / Debug Configuration so that you can debug the plugin in PhpStorm.

Create a project

File | new project: Select "Create from scratch", Enter the name, select the type of Plugin Module, select the SDK that we configured earlier, create the project.

Add pathetic copyrights to the plugin.xml file (without this in any way!)

<name>KohanaStorm</name> <description>KohanaStorm framework integration for PhpStorm<br/> Authors: zenden2k@gmail.com </description> <version>0.1</version> <vendor url="http://zenden.ws/" email="zenden2k@gmail.com">zenden.ws</vendor> <idea-version since-build="8000"/> 


In order for our plugin to be launched not only under IDEA, but also in PhpStorm, we add the following dependency in plugin.xml:
 <depends>com.intellij.modules.platform</depends> 

The basics


For each file, IntelliJ IDEA builds a PSI tree.

PSI (Program Structure Interface) is a structure that represents the contents of a file as a hierarchy of elements of a particular programming language. PsiFile is a common parent class for all PSI files, and specific programming languages ​​are represented as classes inherited from PsiFile. For example, the PsiJavaFile class represents a java file, the XmlFile class represents an XML file. The PSI tree can be viewed using the PSI Viewer tool (Tools -> View PSI Structure):

image

Plugin development


So, I wanted to be able to go from the controller file by Ctrl + B (or Ctrl + Click) via View :: factory ('template_name') directly to the template file.



How to implement your plans?



To resolve links, we need to create 3 classes inherited from:

PsiReference - an object that implements this interface is a link. It contains data on the location in the parent element (position in the text) and data (link text), allowing further "to allow the link." The link should be able to resolve itself, i.e. her resolve () method must be able to find the element to which it points.

PsiReferenceProvider is a class that finds links inside a single PSI element of a tree. It returns an array of PsiReference objects.

PsiReferenceContributor is the class that will register our PsiReferenceProvider as a PSI item handler.

1. Create a reference class MyReference that implements the PsiReference interface, and override the following methods in it


 public class MyReference implements PsiReference { @Override public String toString() { } public PsiElement getElement() { } public TextRange getRangeInElement() { return textRange; } public PsiElement handleElementRename(String newElementName) } public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException { } public boolean isReferenceTo(PsiElement element) { return resolve() == element; } public Object[] getVariants() { return new Object[0]; } public boolean isSoft() { return false; } @Nullable public PsiElement resolve() { } @Override public String getCanonicalText() { } } 


In this class, the most important is the resolve () method. In it we have to return those elements to which our link points. In our case, we return a link to the php file, but in general it can be any element of the psi- tree or language model above it, for example, class, method, variable, etc.

2. Create a class inherited from PsiReferenceProvider and override the getReferencesByElement method:


 public class MyPsiReferenceProvider extends PsiReferenceProvider { @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull final ProcessingContext context) { } } 


The getReferencesByElement method should return a list of references ( PsiReference ) that are contained in the PsiElement element passed to it . In our case, only one link is returned, but in general there may be several, and each link will have to contain a corresponding textRange (starting index and ending index for finding the link within the text of the psi element)

The main problem with the development of this method was that JetBrains did not open access to the language API (in our case, PHP). But here came the help of Reflection. What do we know about the element object? That it must be an instance of the StringLiteralExpressionImpl class.

  public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull final ProcessingContext context) { Project project = element.getProject(); PropertiesComponent properties = PropertiesComponent.getInstance(project); String kohanaAppDir = properties.getValue("kohanaAppPath", "application/"); VirtualFile appDir = project.getBaseDir().findFileByRelativePath(kohanaAppDir); if (appDir == null) { return PsiReference.EMPTY_ARRAY; } String className = element.getClass().getName(); Class elementClass = element.getClass(); // ,     StringLiteralExpressionImpl if (className.endsWith("StringLiteralExpressionImpl")) { try { //   getValueRange,    ,      Method method = elementClass.getMethod("getValueRange"); Object obj = method.invoke(element); TextRange textRange = (TextRange) obj; Class _PhpPsiElement = elementClass.getSuperclass().getSuperclass().getSuperclass(); //   getText,    PHP- Method phpPsiElementGetText = _PhpPsiElement.getMethod("getText"); Object obj2 = phpPsiElementGetText.invoke(element); String str = obj2.toString(); String uri = str.substring(textRange.getStartOffset(), textRange.getEndOffset()); int start = textRange.getStartOffset(); int len = textRange.getLength(); // ,     PHP- (  )   if (uri.endsWith(".tpl") || uri.startsWith("smarty:") || isViewFactoryCall(element)) { PsiReference ref = new MyReference(uri, element, new TextRange(start, start + len), project, appDir); return new PsiReference[]{ref}; } } catch (Exception e) { } } return PsiReference.EMPTY_ARRAY; } 


To determine that we did not just catch the PHP literal, but the string passed exactly in View :: factory (), we again use reflection magic:

 public static boolean isViewFactoryCall(PsiElement element) { PsiElement prevEl = element.getParent(); String elClassName; if (prevEl != null) { elClassName = prevEl.getClass().getName(); } prevEl = prevEl.getParent(); if (prevEl != null) { elClassName = prevEl.getClass().getName(); if (elClassName.endsWith("MethodReferenceImpl")) { try { Method phpPsiElementGetName = prevEl.getClass().getMethod("getName"); String name = (String) phpPsiElementGetName.invoke(prevEl); if (name.toLowerCase().equals("factory")) { Method getClassReference = prevEl.getClass().getMethod("getClassReference"); Object classRef = getClassReference.invoke(prevEl); PrintElementClassDescription(classRef); String phpClassName = (String) phpPsiElementGetName.invoke(classRef); if (phpClassName.toLowerCase().equals("view")) { return true; } } } catch (Exception ex) { } } } return false; } 


To make it clearer what we are dealing with, the picture:

This code determines that our element is really nested in the method call (MethodReference), which is called “factory” and is in the “view” class.

3. Create a class inherited from PsiReferenceContributor and override the following method:


  @Override public void registerReferenceProviders(PsiReferenceRegistrar registrar) { registrar.registerReferenceProvider(StandardPatterns.instanceOf(PsiElement.class), provider); } 


All that our class does is register our PsiReferenceProvider in a certain registry, and set a template, to which type (subclass) of PsiElement it should be applied. If the element of the document we need was, say, the value of an XML attribute, everything would be simpler:

  registrar.registerReferenceProvider(StandardPatterns.instanceOf(XmlAttributeValue.class), provider); 


But since JetBrains did not open access to the language API (in our case, PHP), we have to subscribe to absolutely all elements of PsiElement in order to then dynamically determine whether this element is needed or not.

4. Register the Contributor in the plugin.xml file:

  <extensions defaultExtensionNs="com.intellij"> <psi.referenceContributor implementation="MyPsiReferenceContributor"/> </extensions> 


Create a settings page





In phpstorm settings are of two types - related to the project and global. To create a settings page for our plugin, create a class KohanaStormSettingsPage that implements the Configurable interface. The getDisplayName method should return the name of the tab that will appear in the list of PhpStorm settings. The createComponent method should return our form. In the apply method, we must save all the settings.

 public class KohanaStormSettingsPage implements Configurable { private JTextField appPathTextField; private JCheckBox enableKohanaStorm; private JTextField secretKeyTextField; Project project; public KohanaStormSettingsPage(Project project) { this.project = project; } @Nls @Override public String getDisplayName() { return "KohanaStorm"; } @Override public JComponent createComponent() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout (panel, BoxLayout.Y_AXIS)); JPanel panel1 = new JPanel(); panel1.setLayout(new BoxLayout(panel1, BoxLayout.X_AXIS)); enableKohanaStorm = new JCheckBox("Enable Kohana Storm for this project"); ... PropertiesComponent properties = PropertiesComponent.getInstance(project); appPathTextField.setText(properties.getValue("kohanaAppPath", DefaultSettings.kohanaAppPath)); return panel; } @Override public void apply() throws ConfigurationException { PropertiesComponent properties = PropertiesComponent.getInstance(project); properties.setValue("kohanaAppPath", appPathTextField.getText()); properties.setValue("enableKohanaStorm", String.valueOf(enableKohanaStorm.isSelected()) ); properties.setValue("kohanaStormSecretKey", secretKeyTextField.getText()); } @Override public boolean isModified() { return true; } @Override public String getHelpTopic() { return null; } @Override public void disposeUIResources() { } @Override public void reset() { } } 


Register our settings page in the plugin.xml file:

 <extensions defaultExtensionNs="com.intellij"> <psi.referenceContributor implementation="MyPsiReferenceContributor"/> <projectConfigurable implementation="KohanaStormSettingsPage"></projectConfigurable > </extensions> 


(if we had a global settings page, we would use applicationConfigurable)

Storage settings

The least confusing way to store settings for a plugin is to use the PropertiesComponent class and the setValue and getValue methods. A more complicated way is described in the documentation.

Plug-in installation

After the plugin development is complete, you must complete
Build -> Prepare plugin for deployment. After that, a file with the name jar will appear in the project folder, which can be distributed.
You can install it in phpstorm by running (File-> Settings-> Plugins-> Install From Disk)

Download plugin and source codes

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


All Articles