Я работаю веб-программистом, пишу на PHP и использую фреймворк Kohana. Для разработки использую потрясающую, на мой взгляд, среду PhpStorm.
При работе с большими и не очень проектами меня всегда угнетало, что я много времени трачу на навигацию по проекту, на поиск того или иного файла (контроллера или шаблона) в дереве проекта. Ctrl+Shift+N, к сожалению, удобен далеко не всегда.
Для начала мне захотелось сделать так, чтобы можно было переходить из файла контроллера по нажатию 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 (без этого никак!)
Чтобы наш плагин запускался не только под IDEA, но и в PhpStorm, добавим в plugin.xml следующую зависимость:
Для каждого файла 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 элементов.
В этом классе самое большое значение имеет метод resolve(). В нем мы должны вернуть те элементы, на которые указывает наша ссылка. В нашем случае мы возвращаем ссылку на php-файл, но в общем случае это может быть любой элемент psi- дерева или языковой модели, лежащей над ним, например класс, метод, переменная и т.д.
Метод getReferencesByElement должен возвратить список ссылок (PsiReference), которые содержатся в переданном ему элементу PsiElement. В нашем случае возвращается только одна ссылка, но в общем случае их может быть несколько, при этом каждая ссылка должна будет содержать соответствующий textRange (начальный индекс и конечный индекс нахождения ссылки внутри текста psi-элемента)
Основной проблемой при разработке этого метода стало то, что JetBrains не открыла плагинам доступа к языковому API (в нашем случае PHP). Но тут на помощь пришел Reflection. Что мы знаем об объекте element? То, что он должен быть экземпляром класса StringLiteralExpressionImpl.
Чтобы определить, что нам попался не просто PHP-литерал, а строка, переданная именно в View::factory(), снова воспользуемся магией рефлекшн:
Чтобы было понятнее, с чем мы имеем дело, картинка:
Данный код определяет, что наш элемент действительно вложен в вызов метода (MethodReference), который называется «factory» и находится в классе «view».
Всё, что делает наш класс — регистрирует наш PsiReferenceProvider в неком реестре, и задает шаблон, к какому типу (подклассу) PsiElement его надо применять. Если бы нужный нам элемент документа был, скажем, значением XML-атрибута, всё было бы проще:
Но поскольку JetBrains не открыла доступа к языковому API (в нашем случае PHP), нам приходится подписываться на абсолютно все элементы PsiElement, чтобы затем динамически определить, нужный нам это элемент или нет.
В phpstorm настройки бывают двух типов — относящиеся к проекту и глобальные. Чтобы создать страницу настроек для нашего плагина, создадим класс KohanaStormSettingsPage, реализующий интерфейс Configurable. Метод getDisplayName должен возвращать название таба, которое будет отображаться в списке настроек PhpStorm. Метод createComponent должен возвращать нашу форму. В методе apply мы должны сохранить все настройки.
Зарегистрируем нашу страницу настроек в файле plugin.xml:
(если бы мы наша страница настроек была глобальной, мы бы использовали applicationConfigurable)
Наименее замороченный способ хранения настроек для плагина — использование класса PropertiesComponent и методов setValue и getValue. Более сложный способ описан в документации.
После того, как разработка плагина будет завершена, необходимо выполнить
Build -> Prepare plugin for deployment. После этого в папке проекта появится файл с именем jar, который можно будет распространять.
Установить в phpstorm его можно выполнив (File->Settings->Plugins->Install From Disk)
Скачать плагин и исходные коды
При работе с большими и не очень проектами меня всегда угнетало, что я много времени трачу на навигацию по проекту, на поиск того или иного файла (контроллера или шаблона) в дереве проекта. Ctrl+Shift+N, к сожалению, удобен далеко не всегда.
Для начала мне захотелось сделать так, чтобы можно было переходить из файла контроллера по нажатию 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), который называется «factory» и находится в классе «view».
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)
Скачать плагин и исходные коды