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 spend 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 I could navigate from the controller file by pressing Ctrl + B (or Ctrl + Click) above the name of the template passed to the Kokhanovsky View :: factory () directly into the template file:


     
    Therefore, I decided to write a small plugin for PhpStorm, which 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 plugins is very scarce, it is recommended that you also get a copy of the Intellij IDEA source codes and use it as visual documentation :)

    Tool setup:


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

    - select the Project tab
    - click on new, then IntelliJ IDEA Plugin SDK and in the menu that opens - select the path to PhpStorm (it is also possible to IntelliJ IDEA, but then we won’t be able to debug plugin in PhpStorm)


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

    Create a project

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

    Add pathos copyrights to the plugin.xml file (without this, nothing!)

        <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"/>
    


    To make our plugin run not only under IDEA, but also in PhpStorm, add the following dependency to plugin.xml:
    <depends>com.intellij.modules.platform</depends>

    The basics


    For each file, IntelliJ IDEA builds a PSI tree.

    A PSI (Program Structure Interface) is a structure that represents the contents of a file as a hierarchy of elements in a particular programming language. PsiFile is the 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 switch from the controller file by Ctrl + B (or Ctrl + Click) by View :: factory ('template_name') directly to the template file.


     

    How to implement the plan?



    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), which later allows “allow the link”. The link must be able to resolve itself, i.e. her resolve () method should be able to find the element she points to.

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

    PsiReferenceContributor - a class that will register our PsiReferenceProvider as a handler of PSI elements.

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


    publicclassMyReferenceimplementsPsiReference{
    @Overridepublic String toString(){    
            }
            public PsiElement getElement(){
            }
            public TextRange getRangeInElement(){
                return textRange;
            }
            public PsiElement handleElementRename(String newElementName)      
            }
            public PsiElement bindToElement(PsiElement element)throws IncorrectOperationException { 
            }
            publicbooleanisReferenceTo(PsiElement element){
                return resolve() == element;
            }
            public Object[] getVariants() {
                returnnew Object[0];
            }
            publicbooleanisSoft(){
                returnfalse;
            }
        @Nullablepublic PsiElement resolve(){
        }
        @Overridepublic String getCanonicalText(){
        }
    }
    


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

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


    publicclassMyPsiReferenceProviderextendsPsiReferenceProvider{ 
    @Overridepublic PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNullfinal ProcessingContext context) {
    }
    }
    


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

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

    public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNullfinal 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();
            // определяем, что объект является экземпляром StringLiteralExpressionImplif (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);
                        returnnew PsiReference[]{ref};
                    }
                } catch (Exception e) {
                }
            }
            return PsiReference.EMPTY_ARRAY;
        }
    


    To determine that we were caught not just in a PHP literal, but in a string passed to View :: factory (), we again use the magic of reflection:

    publicstaticbooleanisViewFactoryCall(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")) {
                                returntrue;
                            }
                        }
                    } catch (Exception ex) {
                    }
                }
            }
            returnfalse;
        }
    


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

    This code determines that our element is really embedded in a method call (MethodReference), which is called a "factory" and is located in the "view" class.

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


    @OverridepublicvoidregisterReferenceProviders(PsiReferenceRegistrar registrar){
            registrar.registerReferenceProvider(StandardPatterns.instanceOf(PsiElement.class), provider);
        }
    


    All that our class does is register our PsiReferenceProvider in a registry, and set the template to which type (subclass) of PsiElement it should be applied. If the document element we needed were, 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 PsiElement elements in order to then dynamically determine whether we need this element or not.

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

    <extensionsdefaultExtensionNs="com.intellij"><psi.referenceContributorimplementation="MyPsiReferenceContributor"/></extensions>


    Create a settings page




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

    publicclassKohanaStormSettingsPageimplementsConfigurable{
        private JTextField appPathTextField;
        private JCheckBox enableKohanaStorm;
        private JTextField secretKeyTextField;
        Project project;
        publicKohanaStormSettingsPage(Project project){
            this.project = project;
        }
        @Nls@Overridepublic String getDisplayName(){
            return"KohanaStorm";
        }
        @Overridepublic 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;
        }
        @Overridepublicvoidapply()throws ConfigurationException {
            PropertiesComponent properties = PropertiesComponent.getInstance(project);
            properties.setValue("kohanaAppPath", appPathTextField.getText());
            properties.setValue("enableKohanaStorm", String.valueOf(enableKohanaStorm.isSelected()) );
            properties.setValue("kohanaStormSecretKey", secretKeyTextField.getText());
        }
        @OverridepublicbooleanisModified(){
            returntrue;
        }
        @Overridepublic String getHelpTopic(){
            returnnull;
        }
        @OverridepublicvoiddisposeUIResources(){
        }
        @Overridepublicvoidreset(){
        }
    }
    


    We register our settings page in the plugin.xml file:

    <extensionsdefaultExtensionNs="com.intellij"><psi.referenceContributorimplementation="MyPsiReferenceContributor"/><projectConfigurableimplementation="KohanaStormSettingsPage"></projectConfigurable ></extensions>


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

    Settings Storage

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

    Plugin installation

    After the development of the plugin is completed, you need to do
    Build -> Prepare plugin for deployment. After that, a file called jar will appear in the project folder, which can be distributed.
    You can install it in phpstorm by executing (File-> Settings-> Plugins-> Install From Disk)

    Download the plugin and source codes

    Also popular now: