Создание плагина разрешения ссылок для PhpStorm (IntelliJ IDEA)

от автора

Я работаю веб-программистом, пишу на PHP и использую фреймворк Kohana. Для разработки использую потрясающую, на мой взгляд, среду PhpStorm.

При работе с большими и не очень проектами меня всегда угнетало, что я много времени трачу на навигацию по проекту, на поиск того или иного файла (контроллера или шаблона) в дереве проекта.

Для начала мне захотелось сделать так, чтобы можно было переходить из файла контроллера по нажатию Ctrl+B (или Ctrl+Click) над именем шаблона, передаваемого в кохановский View::factory(), непосредственно в файл шаблона:


 
Поэтому я решил написать небольшой плагин для PhpStorm, который облегчил бы мою работу и освободил бы от некоторой части рутины.

Подготовка окружения

Нам потребуются:
IntelliJ IDEA Community Edition или Ultimate.
JDK ( необходимо скачать версию, с которой собран PhpStorm, иначе плагин не запустится, в моем случае это была Java 1.6);

Поскольку документация по созданию плагинов IDEA очень скудна, рекомендуется также обзавестись копией исходных кодов Intellij IDEA, и использовать ее в качестве наглядной документации 🙂

Настройка инструментов:

Необходимо настроить Java SDK и IntelliJ IDEA Plugin SDK:
— запускаем IntelliJ IDEA
— открываем пункт меню File | Project Structure
— выбираем вкладку SDKs, жмем на плюсик и выбираем путь к JDK

— выбираем вкладку Project
— нажимаем на new, далее IntelliJ IDEA Plugin SDK и в открывшемся меню — выбираем путь к PhpStorm (можно и к IntelliJ IDEA, но тогда мы не сможем отлаживать плагин в PhpStorm)


Также необходимо создать Run/Debug Configuration, чтобы можно было отлаживать плагин в PhpStorm.

Создадим проект

File | new project: Выбираем «Create from scratch», Вводим имя, выбираем тип Plugin Module, выбираем SDK, который мы настроили ранее, создаем проект.

Добавляем пафосные копирайты в файл plugin.xml (без этого никак!)

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

Чтобы наш плагин запускался не только под IDEA, но и в PhpStorm, добавим в plugin.xml следующую зависимость:

<depends>com.intellij.modules.platform</depends> 

Основы

Для каждого файла IntelliJ IDEA строит дерево PSI.

PSI (Program Structure Interface) — это структура, представляющая содержимое файла как иерархию элементов определенного языка программирования. PsiFile является общим родительским классом для всех PSI файлов, а конкретные языки программирования представлены в виде классов, унаследованных от PsiFile. Например, класс PsiJavaFile представляет файл java, класс XmlFile представляет XML файл. Дерево PSI можно посмотреть, используя инструмент PSI Viewer (Tools -> View PSI Structure):

image

Разработка плагина

Итак, мне захотелось, чтобы можно было переходить из файла контроллера по Ctrl+B (или Ctrl+Click) по View::factory(‘имя_шаблона’) непосредственно в файл шаблона.


 

Как реализовать задуманное?

Для разрешения ссылок нам нужно создать 3 класса, унаследованных от:

PsiReference — объект, реализующий этот интерфейс, представляет собой ссылку. Он содержит в себе данные о местонахождении в родительском элементе (положение в тексте) и данные (текст ссылки), позволяющие в дальнейшем «разрешить ссылку». Ссылка должна уметь сама себя разрешать, т.е. ее метод resolve() должен уметь найти элемент, на который она указывает.

PsiReferenceProvider — класс, который находит ссылки внутри одного элемента PSI дерева. Он возвращает массив объектов PsiReference.

PsiReferenceContributor — класс, который будет регистрировать наш PsiReferenceProvider как обработчик PSI элементов.

1. Создаем класс ссылки MyReference, реализующий интерфейс PsiReference, и в нем переопределить следующие методы
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() {     } } 

В этом классе самое большое значение имеет метод resolve(). В нем мы должны вернуть те элементы, на которые указывает наша ссылка. В нашем случае мы возвращаем ссылку на php-файл, но в общем случае это может быть любой элемент psi- дерева или языковой модели, лежащей над ним, например класс, метод, переменная и т.д.

2. Создаем класс, унаследованный от PsiReferenceProvider и переопределить метод getReferencesByElement:
public class MyPsiReferenceProvider extends PsiReferenceProvider {  @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull final ProcessingContext context) { } } 

Метод getReferencesByElement должен возвратить список ссылок (PsiReference), которые содержатся в переданном ему элементу PsiElement. В нашем случае возвращается только одна ссылка, но в общем случае их может быть несколько, при этом каждая ссылка должна будет содержать соответствующий textRange (начальный индекс и конечный индекс нахождения ссылки внутри текста psi-элемента)

Основной проблемой при разработке этого метода стало то, что JetBrains не открыла плагинам доступа к языковому API (в нашем случае PHP). Но тут на помощь пришел Reflection. Что мы знаем об объекте element? То, что он должен быть экземпляром класса StringLiteralExpressionImpl.

 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;     } 

Чтобы определить, что нам попалась не просто PHP- литерал, а строка, переданная именно в View::factory(), снова воспользуемся магией рефлекшн:

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;     } 

Чтобы было понятнее, с чем мы имеем дело, картинка:

Данный код определяет, что наш элемент действительно вложен в вызов метода (MethodReference), который называется «view» и находится в классе «factory».

3. Создать класс, унаследованный от PsiReferenceContributor и переопределить следующий метод:
   @Override     public void registerReferenceProviders(PsiReferenceRegistrar registrar) {         registrar.registerReferenceProvider(StandardPatterns.instanceOf(PsiElement.class), provider);     } 

Всё, что делает наш класс — регистрирует наш PsiReferenceProvider в неком реестре, и задает шаблон, к какому типу (подклассу) PsiElement его надо применять. Если бы нужный нам элемент документа был, скажем, значением XML-атрибута, всё было бы проще:

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

Но поскольку JetBrains не открыла доступа к языковому API (в нашем случае PHP), нам приходится подписываться на абсолютно все элементы PsiElement, чтобы затем динамически определить, нужный нам это элемент или нет.

4. Зарегистрировать Contributor в файле plugin.xml:

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

Создаем страницу настроек


 
В phpstorm настройки бывают двух типов — относящиеся к проекту и глобальные. Чтобы создать страницу настроек для нашего плагина, создадим класс KohanaStormSettingsPage, реализующий интерфейс Configurable. Метод getDisplayName должен возвращать название таба, которое будет отображаться в списке настроек PhpStorm. Метод createComponent должен возвращать нашу форму. В методе apply мы должны сохранить все настройки.

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() {      } } 

Зарегистрируем нашу страницу настроек в файле plugin.xml:

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

(если бы мы наша страница настроек была глобальной, мы бы использовали applicationConfigurable)

Хранение настроек

Наименее замороченный способ хранения настроек для плагина — использование класса PropertiesComponent и методов setValue и getValue. Более сложный способ описан в документации.

Установка плагина

После того, как разработка плагина будет завершена, необходимо выполнить
Build -> Prepare plugin for deployment. После этого в папке проекта появится файл с именем jar, который можно будет распространять.
Установить в phpstorm его можно выполнив (File->Settings->Plugins->Install From Disk)

Скачать плагин и исходные коды

ссылка на оригинал статьи http://habrahabr.ru/post/161877/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *