Я хотел бы поделиться простым способом добавления недостающего автодополнения в семейство IDE IDEA. В нашем случае в WebStorm или PhpStrom. Наш фронтенд-проект использует библиотеку require.js. И при работе с ним нужно указывать пути к определенным файлам, чтобы добавить их в качестве зависимостей.
К сожалению, пути к этим файлам приходится прописывать вручную или копировать по частям.
И я подумал, что это надо исправить и добавить автодополнение путей к файлам.
После этого я начал искать информацию о том, как писать плагины для Idea и вспомнил статья хабраузер зенден2к , в котором он рассказал о том, как сделать плагин разрешения ссылок для Kohana. Прежде чем читать мою статью, вам обязательно следует ее прочитать.
Решив, что разрешение ссылок — это тоже очень полезная функция, я сначала написал плагин именно для этого.
При написании плагина я столкнулся с проблемой отсутствия PSI-структуры для javascript-файлов в Idea Community Edition, а без этого не удавалось определить структуру JS-файла, которая нужна для определения необходимого элемента для разрешения.
связь.
Мне пришлось установить Idea Ultimate EAP. В Idea UT необходимо установить плагин для Javascript, после чего в PSI Viewer (Инструменты -> Просмотр структуры PSI) будет доступен выбор структуры PSI для файлов Javascript. Скриншот
Кроме того, в связи с тем, что с момента написания этой статьи компания JetBrains внедрила openapi для PHP и JS, я использовал привязку к определенному элементу PSI JSLiteralExpression. Мой PsiReferenceContributor теперь выглядит так: ТребоватьjsPsiReferenceContributor.java
Как видите, вместо PsiElement.class я уже использовал специально JSLiteralExpression.class, чтобы не пришлось обрабатывать все элементы подряд. Но чтобы иметь возможность использовать openapi, нужно по идее подключить его к проекту плагина.package requirejs; import com.intellij.lang.javascript.psi.JSLiteralExpression; import com.intellij.patterns.StandardPatterns; import com.intellij.psi.PsiReferenceContributor; import com.intellij.psi.PsiReferenceRegistrar; public class RequirejsPsiReferenceContributor extends PsiReferenceContributor { @Override public void registerReferenceProviders(PsiReferenceRegistrar psiReferenceRegistrar) { RequirejsPsiReferenceProvider provider = new RequirejsPsiReferenceProvider(); psiReferenceRegistrar.registerReferenceProvider(StandardPatterns.instanceOf(JSLiteralExpression.class), provider); } }
Для этого вам нужно зайти в «Структуру проекта», выбрать там «Библиотеки».
Нажмите на + над центральным столбцом, выберите Java и в открывшемся окне выбора файла выберите файл "/path_to_webstrom/plugins/JavaScriptLanguage/lib/javascript-openapi.jar": Скриншот
Затем заходим в «Модули», открываем вкладку «Зависимости» и там напротив javascript-openapi указываем Scope как Provided: Скриншот
После этих манипуляций IDE предложит вам имена классов и прочего, что включено в openapi для javascript.
Также пришлось изменить PsiReferenceProvider, избавив его от отражения, получилось примерно так: RequirejsPsiReferenceProvider.java package requirejs;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.javascript.psi.JSCallExpression;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
public class RequirejsPsiReferenceProvider extends PsiReferenceProvider {
@NotNull
@Override
public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {
Project project = psiElement.getProject();
PropertiesComponent properties = PropertiesComponent.getInstance(project);
String webDirPrefString = properties.getValue("web_dir", "webfront/web");
VirtualFile webDir = project.getBaseDir().
findFileByRelativePath(webDirPrefString); if (webDir == null) { return PsiReference.EMPTY_ARRAY; } try { String path = psiElement.getText(); if (isRequireCall(psiElement)) { PsiReference ref = new RequirejsReference(psiElement, new TextRange(1, path.length() - 1), project, webDir); return new PsiReference[] {ref}; } } catch (Exception ignored) {} return new PsiReference[0]; } public static boolean isRequireCall(PsiElement element) { PsiElement prevEl = element.getParent(); if (prevEl != null) { prevEl = prevEl.getParent(); } if (prevEl != null) { if (prevEl instanceof JSCallExpression) { try { if (prevEl.getChildren().
length > 1) { if (prevEl.getChildren()[0].
getText().
toLowerCase().
equals("require")) {
return true;
}
}
} catch (Exception ignored) {}
}
}
return false;
}
}
Далее вам необходимо реализовать метод, отвечающий за разрешение этой ссылки.
И тут у меня возникла проблема, связанная с очень скудной информацией о написании плагинов для идеи.
Дело в том, что изначально у меня было желание начать поиск файлов из каталогов с пометкой «Корень ресурса», но, увы, я не смог найти, как получить такие каталоги.
Поэтому я решил взять путь к каталогу из настроек, для чего реализовал страницу настроек, как описано в статье.
зенден2к , поэтому не буду повторяться.
Как только мы узнали каталог, в котором нам нужно искать файлы по пути, все стало просто.
Класс VirtualFile имеет метод findFileByRelativePath, который принимает в качестве входных данных строку пути и проверяет, существует ли файл по заданному пути, и если да, то возвращает его как экземпляр класса VirtualFile. Поэтому нам пришлось взять строковое значение из PsiElement, вырезать лишнее, добавить недостающее и проверить, существует ли такой файл.
Если он существует, просто верните ссылку на него как на экземпляр PsiElement. Метод разрешения выглядит следующим образом: RequirejsReverence.java::resolve() @Nullable
@Override
public PsiElement resolve() {
String path = element.getText();
path = path.replace("'", "").
replace("\"", ""); if (path.startsWith("tpl!")) { path = path.replace("tpl!", ""); } else { path = path.concat(".
js"); } if (path.startsWith(".
/")) { path = path.replaceFirst( ".
", element .
getContainingFile() .
getVirtualFile() .
getParent() .
getPath() .
replace(webDir.getPath(), "") ); } VirtualFile targetFile = webDir.findFileByRelativePath(path); if (targetFile != null) { return PsiManager.getInstance(project).
findFile(targetFile);
}
return null;
}
Сделав это, я получил разрешение на ссылки и смог приступить к реализации автозаполнения.
По идее, существует два способа реализации автозаполнения.
Первый простой — реализовать метод getVariants интерфейса PsiReference, а второй расширенный — использовать CompletionContributor. Я попробовал оба способа, но преимуществ в CompletionContributor для себя не нашел, поэтому остановился на первом способе.
Для автозаполнения нам нужно вернуть список элементов в виде массива.
Это может быть массив со строками, LookupElement или PsiElement. Вначале я пробовал возвращать строки.
Но тут меня ждал сюрприз.
Все дело в том, что идея строк со слэшами вставляет всю строку после последнего слэша.
Более того, если вы создаете строку только со значением после косой черты, то idea не воспринимает эту строку как подходящую для автозаполнения.
Мне такое поведение не совсем понятно.
И мне не удалось найти информацию о том, как правильно автозаполнять строки косыми чертами или, как вариант, путями к файлам.
Поэтому я сделал это по-своему.
Чтобы самостоятельно контролировать вставку значения, необходимо реализовать интерфейс InsertHandler и выполнить необходимые действия в методе handleInsert. И чтобы его использовать, нужно вернуть не просто строку, а LookupElement, который будет содержать нужный нам InsertHandler.
Поэтому я расширил класс LookupElement следующим образом: RequirejsLookupElement.java package requirejs;
import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
public class RequirejsLookupElement extends LookupElement {
String path;
PsiElement element;
private InsertHandler<LookupElement> insertHandler = null;
public RequirejsLookupElement(String path, InsertHandler<LookupElement> insertHandler, PsiElement element) {
this.path = path;
this.insertHandler = insertHandler;
this.element = element;
}
public void handleInsert(InsertionContext context) {
if (this.insertHandler != null) {
this.insertHandler.handleInsert(context, this);
}
}
@NotNull
@Override
public String getLookupString() {
return path;
}
}
Реализация InsertHandler выглядит следующим образом: RequirejsInsertHandler.java package requirejs;
import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
public class RequirejsInsertHandler implements InsertHandler {
private static final RequirejsInsertHandler instance = new RequirejsInsertHandler();
@Override
public void handleInsert(InsertionContext insertionContext, LookupElement lookupElement) {
if (lookupElement instanceof RequirejsLookupElement) {
insertionContext.getDocument().
replaceString( ((RequirejsLookupElement) lookupElement).
element.getTextOffset() + 1, insertionContext.getTailOffset(), ((RequirejsLookupElement) lookupElement).
path
);
}
}
public static RequirejsInsertHandler getInstance() {
return instance;
}
}
Суть метода handleInsert в том, что мы берём LookupElement, получаем PsiElement, для которого он был показан и выбран, из PsiElement получаем его местоположение в файле и заменяем всю длину строки элемента текстом из LookupElement.path. Конечно это не лучший способ, но другого мне, к сожалению, найти не удалось.
После этого я нашел все соответствующие файлы и вернул их в виде массива LookupElement.
Вот полный список RequirejsReference: ТребоватьjsReference.java package requirejs;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileImpl;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiReference;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
public class RequirejsReference implements PsiReference {
PsiElement element;
TextRange textRange;
Project project;
VirtualFile webDir;
public RequirejsReference(PsiElement element, TextRange textRange, Project project, VirtualFile webDir) {
this.element = element;
this.textRange = textRange;
this.project = project;
this.webDir = webDir;
}
@Override
public PsiElement getElement() {
return this.element;
}
@Nullable
@Override
public PsiElement resolve() {
String path = element.getText();
path = path.replace("'", "").
replace("\"", ""); if (path.startsWith("tpl!")) { path = path.replace("tpl!", ""); } else { path = path.concat(".
js"); } if (path.startsWith(".
/")) { path = path.replaceFirst( ".
", element .
getContainingFile() .
getVirtualFile() .
getParent() .
getPath() .
replace(webDir.getPath(), "") ); } VirtualFile targetFile = webDir.findFileByRelativePath(path); if (targetFile != null) { return PsiManager.getInstance(project).
findFile(targetFile); } return null; } @Override public String toString() { return getCanonicalText(); } @Override public boolean isSoft() { return false; } @NotNull @Override public Object[] getVariants() { ArrayList<String> files = filterFiles(this.element); ArrayList<LookupElement> completionResultSet = new ArrayList<LookupElement>(); for (int i = 0; i < files.size(); i++) { completionResultSet.add( new RequirejsLookupElement( files.get(i), RequirejsInsertHandler.getInstance(), this.element ) ); } return completionResultSet.toArray(); } protected ArrayList<String> getAllFilesInDirectory(VirtualFile directory) { ArrayList<String> files = new ArrayList<String>(); VirtualFile[] childrens = directory.getChildren(); if (childrens.length != 0) { for (int i = 0; i < childrens.length; i++) { if (childrens[i] instanceof VirtualDirectoryImpl) { files.addAll(getAllFilesInDirectory(childrens[i])); } else if (childrens[i] instanceof VirtualFileImpl) { files.add(childrens[i].
getPath().
replace(webDir.getPath() + "/", "")); } } } return files; } protected ArrayList<String> filterFiles (PsiElement element) { String value = element.getText().
replace("'", "").
replace("\"", "").
replace("IntellijIdeaRulezzz ", ""); Boolean tpl = value.startsWith("tpl!"); String valuePath = value.replaceFirst("tpl!", ""); ArrayList<String> allFiles = getAllFilesInDirectory(webDir); ArrayList<String> trueFiles = new ArrayList<String>(); String file; for (int i = 0; i < allFiles.size(); i++) { file = allFiles.get(i); if (file.startsWith(valuePath)) { if (tpl && file.endsWith(".
html")) { trueFiles.add("tpl!" + file); } else if (file.endsWith(".
js")) { trueFiles.add(file.replace(".
js", ""));
}
}
}
return trueFiles;
}
@Override
public boolean isReferenceTo(PsiElement psiElement) {
return false;
}
@Override
public PsiElement bindToElement(@NotNull PsiElement psiElement) throws IncorrectOperationException {
throw new IncorrectOperationException();
}
@Override
public PsiElement handleElementRename(String s) throws IncorrectOperationException {
throw new IncorrectOperationException();
}
@Override
public TextRange getRangeInElement() {
return textRange;
}
@NotNull
@Override
public String getCanonicalText() {
return element.getText();
}
}
Отдельно я выделил метод поиска файлов, так как он рекурсивный, а также выделил метод фильтрации файлов, так как для шаблонов нужен только html, а для остального нужны js файлы.
Также при вставке шаблоны вставляются вместе с tpl! префикс, а файлы js вставляются без расширения js. УПД : В комментариях пользователь ВИСТАЛЛ , предположил, что создание собственного класса-потомка LookupElement не является необходимым.
Вместо этого вы можете использовать LookupElementBuilder, который позволяет вам указать, какой InsertHandler использовать и какому PsiElement он принадлежит.
Чтобы использовать LookupElementBuilder, я изменил метод RequirejsReference::getVariants следующим образом: RequirejsReference::getVariants @NotNull
@Override
public Object[] getVariants() {
ArrayList<String> files = filterFiles(element);
ArrayList<LookupElement> completionResultSet = new ArrayList<LookupElement>();
for (int i = 0; i < files.size(); i++) {
completionResultSet.add(
LookupElementBuilder
.
create(element, files.get(i)) .
withInsertHandler(
RequirejsInsertHandler.getInstance()
)
);
}
return completionResultSet.toArray();
}
Чтобы сгенерированный LookupElement знал, к какому PsiElement он принадлежит, просто вызовите метод create, передав PsiElement в качестве первого параметра, а строку, которую необходимо использовать для автозаполнения, — в качестве второго.
Я также изменил сам RequirejsInsertHandler::handleInsert следующим образом: RequirejsInsertHandler::handleInsert @Override
public void handleInsert(InsertionContext insertionContext, LookupElement lookupElement) {
insertionContext.getDocument().
replaceString( lookupElement.getPsiElement().
getTextOffset() + 1,
insertionContext.getTailOffset(),
lookupElement.getLookupString()
);
}
Из него я удалил проверку типа LookupElement и использовал методы для получения PsiElement и строки замены.
После этих манипуляций класс RequirejsLookupElement больше не нужен.
УПД 2 : Плагин был немного обновлен и выложен на github: github.com/Fedott/WebStormRequireJsPlugin Тот же плагин теперь доступен в официальном репозитории jetbrains: плагины.
jetbrains.com/plugin/7337 Вы можете отправить свой список пожеланий на github или сюда.
Вот и все.
Если у вас есть вопросы или советы, как лучше это реализовать, буду рад их прочитать.
Теги: #webstorm #phpstorm #idea #JavaScript #java #requirejs #JavaScript #programming #java
-
Опыт Использования Pdfedit В Kubuntu.
19 Oct, 24