Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Плагин для webstorm и авто-дополнение

Anna | 5.06.2014 | нет комментариев

Хотелось бы поделиться простым методом добавить недостающее авто-дополнение в IDE семейства IDEA. В нашем случае к WebStorm либо PhpStrom.

У нас на плане на фронте применяется библиотека require.js. И при работе с ней необходимо указывать пути к тем либо другим файлам что бы добавить их в зависимости. К сожалению пути к этим файлам доводиться писать руками либо копировать по частям.
И я подумал что это необходимо бы поправить, и добавить авто дополнение пути до файлов.

Позже этого я начал искать информацию как писать плагины к Idea и припомнил о статье програюзераzenden2k, в которой он рассказывал как сделать плагин для разрешения ссылок для kohana. Перед прочтением моей статьи непременно необходимо её прочитать.

Решив что разрешение ссылок тоже дюже пригодный функционал, я для начала написал плагин именно для этого.
При написании плагина я столкнулся с задачей отсутствия PSI конструкции для javascript файлов в Idea Community Edition, а без этого было не допустимо определить конструкцию файла JS, которая необходима для определения надобного элемента для разрешения ссылки. Мне пришлось поставить себе Idea Ultimate EAP. В Idea UT необходимо поставить плагин для Javascript, и тогда в PSI Viewer (Tools ->View PSI Structure) будет доступен выбор PSI конструкции для Javascript файлов.

Скриншот

image

Так же в связи с тем что со времени написания той статьи JetBrains выкатили openapi для PHP и JS, я применял привязку теснее к определенному PSI элементу JSLiteralExpression. Мой PsiReferenceContributor стал выглядеть так:

RequirejsPsiReferenceContributor.java

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

Как видно взамен PsiElement.class я теснее применял реально JSLiteralExpression.class, что бы не доводилось обрабатывать все подряд элементы.
Но для того что бы дозволено было применять openapi необходимо подключить его в плане плагина в idea. Для этого необходимо зайти в Project Structure, там предпочесть Libraries. Нажимаем на над центральной колонкой, выбираем Java и в открывшемся окне выбора файла выбираем файл “/path_to_webstrom/plugins/JavaScriptLanguage/lib/javascript-openapi.jar”:

Скриншот

image

Потом переходим к Modules, открываем вкладку Dependencies, и там на вопреки javascript-openapi указываем Scope как Provided:

Скриншот

image

Позже этих манипуляций 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;
    }
}

Дальше нужно реализовать способ отвечающий за разрешение данной ссылки.
И вот здесь у меня появился затык связанный с дюже скупой информацией о написании плагинов к idea. Дело в том что первоначально у меня было желание начинать поиск файлов от директорий помеченных как «Resource Root», но увы я не сумел обнаружить как дозволено получить такие директории. Следственно решил брать путь до директории из настроек, для чего реализовал страницу настройки как в описано в статьеzenden2k, следственно не буду повторяться.
Позже того как мы узнали директорию в которой нам необходимо искать файлы по пути, всё было легко. У класса VirtualFile есть способ findFileByRelativePath, тот, что принимает на вход строку пути, и ищет существует ли файл по данному пути и есть да, то возвращает его как экземпляр VirtualFile класса. Так что необходимо было взять значение строки из PsiElement, вырезать лишнее, добавить недостающее и проверить существует ли такой файл. Если существует, то легко воротить ссылку на него в виде экземпляра PsiElement. Способ resolve выглядит так:

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

Сделав это я получил разрешение ссылок и дозволено было приступать к реализации авто-дополнения.

В idea есть два метода реализовать авто-дополнение. 1-й примитивный, это реализовать способ getVariants интерфейса PsiReference, и 2-й продвинутый применять CompletionContributor. Я в испробовал оба метода, но каких-то превосходств в CompletionContributor, для себя не нашёл, следственно остановился на применении первого метода.
Для авто-дополнения нам необходимо возвращать список элементов в виде массива. Это может быть массив со строками, LoookupElement либо PsiElement.
В начале я испробовал возвращать строки. Но здесь меня ожидал сюрприз. Дело в том что idea строки со слешами вставляет позже последнего слеша всю строку. При этом если выдавать строку только со значением позже слеша, то 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:

RequirejsReference.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.

На этом всё.
Есть есть вопросы либо советы как отменнее реализовать, буду рад их прочесть.

http://bb3x.ru/blog/25-laravel-tips-and-tricks/

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB