При работе с большими и не очень проектами меня всегда угнетало, что я много времени трачу на навигацию по проекту, на поиск того или иного файла (контроллера или шаблона) в дереве проекта.
Для начала мне захотелось сделать так, чтобы можно было переходить из файла контроллера по нажатию 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):
Разработка плагина
Итак, мне захотелось, чтобы можно было переходить из файла контроллера по 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/
Добавить комментарий