Pull to refresh

Разработка плагина IntelliJ IDEA. Часть 5

Reading time 9 min
Views 12K
Original author: JetBrains
В этой части: подсветка, ссылочная система, автодополнение, навигация по коду. Предыдущая часть здесь.

Подсветка синтаксиса и ошибок


Класс, используемый в IDEA для определения, как соответствующий текстовый диапазон должен быть подсвечен, называется TextAttributesKey. Экземпляр этого класса создается для каждого различного типа элементов, которые должны быть подсвечены (ключевые слова, числа, строки, комментарии и т.д.), он определяет атрибуты по-умолчанию, которые применяются к элементам соответствующего типа (например, ключевые слова выделяются полужирным, числа – синим, строки – курсивом и зеленым фоном). Отображение TextAttributesKey на специфические атрибуты, используемые в редакторе, определено классом EditorColorsScheme и может быть настроено пользователем, если плагин предоставляет соответствующий конфигурационный интерфейс. В подсветке может использоваться наложение нескольких TextAttributeKey: например, один ключ может определять начертание, а другой – цвет элемента.

Базовая подсветка синтаксиса

Подсветка синтаксиса и ошибок выполняется на разных уровнях. На первом – подсветка синтаксиса, основанная на результатах лексического разбора, осуществляется посредством интерфейса SyntaxHighlighter. Этот интерфейс возвращает экземпляры TextAttributeKey для каждого типа токенов, который требует особую подсветку. Для подсвечивания ошибок лексера применяется стандартный объект класса TextAttributeKey для недопустимых символов (HighligherColors.BAD_CHARACTER).

Схема управления цветом немного изменилась в Intelli IDEA 12.1, чтобы облегчить работу дизайнеров схем и сделать одинаковое отображение для различных языков программирования, даже если схема не была изначально предназначена для них. Ранее языковые плагины использовали фиксированные цветовые схемы не всегда совместимые, например, с темными темами. Новая реализация позволяет определить зависимости от набора стандартных текстовых атрибутов, привязанных к схеме, а не конкретному языку. Атрибуты для специфических языков все еще могут быть установлены дизайнером схемы, но теперь они необязательны. Новые цветовые схемы получили расширение .icls во избежание проблем с совместимостью.
Теперь для определения текстовых атрибутов и зависимости от стандартных ключей используется класс DefaultLanguageHighlighterColors.

Пример: реализация SyntaxHighlighlighter для плагина Properties.

На втором уровне подсветки – выделение ошибок, произошедших во время синтаксического разбора. Если определенная цепочка токенов не соответствует грамматике языка, метод PsiBuilder.error() может быть использован для подсветки неверных токенов и отображения сообщения об ошибке.

Аннотации

Третий уровень подсветки выполняется с помощью интерфейса Annotator. Плагин может зарегистрировать одну или несколько аннотаций в точках расширения com.intellij.annotator, после чего они будут вызваны в фоновом процессе во время подсветки элементов PSI-дерева пользовательского языка. Аннотации могут анализировать не только синтаксис, но и семантику и таким образом предоставлять более тонкую логику обработки и подсветки ошибок. Аннотация может содержать функциональность для решения обнаруженных проблем (т.н. quick fix).

Когда файл изменен, аннотация вызывается инкрементально для обработки только изменившихся элементов PSI-дерева.
Для подсветки определенного диапазона текста как ошибки или предупреждения, аннотация вызывает createErrorAnnotation() или createWarningAnnotation() на объекте типа AnnotationHolder, и опционально registerFix() на возвращаемом объекте класса Annotator для добавления логики исправления ошибки. Для применения дополнительной подсветки, аннотация может вызвать AnnotationHolder.createInfoAnnotation() с пустым сообщением и затем, вызвав Annotation.setTextAttributes(), установить атрибуты текста.

Пример: Annotator для языка Properties.

Внешние аннотации

Наконец, если в пользовательском языке используется внешние средства для валидации файлов, их результаты можно предоставить, реализовав интерфейс ExternalAnnotator и зарегистрировав его в точке расширения com.intellij.externalAnnotator. Подсветка с помощью ExternalAnnotator имеет наименьший приоритет и выполняется только после выполнения всех фоновых процессов. Внешние аннотации используют тот же интерфейс AnnotationHolder для адаптации вывода внешних инструментов и показа подсветки.

Страница настроек цвета

Плагин также может предоставить конфигурационный интерфейс, позволяющий пользователю настраивать цвета определенных элементов. Для этого необходимо создать экземпляр класса ColorSettingPage и зарегистрировать его в точке расширения com.intellij.colorSettingsPage.

Пример: ColorSettingsPage для Properties.

Функция «Export to HTML» использует тот же механизм подсветки, что и редактор, поэтому становится доступной для пользовательского языка сразу после реализации SyntaxHighlighter.

Ссылочная система IntelliJ IDEA


Одна из наиболее важных и запутанных частей в реализации программной структуры пользовательского языка – это разрешение ссылок, т.е. способность проследовать от места использования элемента (переменная в выражении, вызов метода и т.д.) к месту определения (декларации переменной, метода и т.п.). Это требуется для поддержки многих функций IDEA, таких как «Go to Declaration» (Ctrl-B и Ctrl-Click), а также при поиске использований, переименовании, автодополнении.

Каждый PSI-элемент, который должен работать ссылкой обязан переопределить метод PsiElement.getReference(), так чтобы он возвращал соответствующую реализацию интерфейса PsiReference. Этот интерфейс может быть реализован как самим классом PsiElement, так и отдельным. Если элемент может содержать несколько ссылок (например, строка с перечислением классов), то в таком случае он должен реализовать метод PsiElement.getReferences(), возвращающий массив из ссылок.

Главный метод интерфейса PsiReference – это resolve(), который возвращает либо элемент, на который указывает ссылка, либо null, если невозможно разрешить ссылку (например, если указывает на неопределенный класс). Противоположный ему метод isReferenceTo(), который проверяет, указывает ли ссылка на конкретный элемент. Последний метод может быть реализован с помощью вызова resolve() и сравнения результата с переданным PSI-элементом, но позволяет разработчику применить дополнительные оптимизации.

Пример: Ссылка на ResourceBundle в плагине Properties.

IDEA предоставляет множество интерфейсов, которые могут быть использованы как база для реализации поддержки ссылок, а именно интерфейс PsiScopeProcessor и метод PsiElement.processDeclarations(). Эти интерфейсы обладают множеством сложностей, которые не обязательны для большинства пользовательских языков (например, поддержка подстановки обобщенных типов), но они необходимы, если пользовательский язык может ссылаться на Java код. Если интероперабельность с Java не требуется, либо имеются другие причины, плагин может переопределить стандартную реализацию разрешения ссылок.

Стандартные вспомогательные классы IDEA, используемые для разрешения ссылок, состоят из следующих компонентов:
  • класса, реализующего интерфейс PsiScopeProcessor, который собирает возможные определения ссылок и останавливает процесс разрешения, когда он полностью завершен. Главный метод, который необходимо реализовать – это execute(), который вызывается для обработки каждого определения и возвращает false, когда определение найдено;
  • функция, которая обходит дерево от места обнаружения ссылки и до ее разрешения или выхода из области видимости;
  • PSI-элементы, на которых вызывается метод processDeclarations() во время обхода PSI-дерева. Если элемент является определением, то он передает ссылку на самого себя в метод execute(). Если необходимо в соответствии с языковыми правилами определения областей видимости, элемент может передать PsiScopeProcessor в свои дочерние элементы.

Существует расширение интерфейса PsiReference, которое позволяет ссылкам использовать несколько целевых элементов – PsiPolyVariantReference. Целевые элементы ссылки возвращаются методом multiResolve(). Действие «Go to Declaration» для такого типа ссылок позволяет выбрать какой именно элемент использовать для навигации. Реализация multiResolve() может быть основана на PsiScopeProcessor, если вместо остановки поиска после первого найденного результата продолжить собирать остальные целевые элементы.

С другой стороны в IntelliJ IDEA существует подход к реализации ссылочной системы посредством Reference Contributor и Reference Provider.
PsiReferenceContributor проверяет каждый PsiElement и по соответствующему описанию, заданному пользователем, возвращает зарегистрированный для данного случая объект провайдера ссылок (пример). В свою очередь, PsiReferenceProvider – это класс, предназначенный для нахождения ссылок внутри одного элемента PSI дерева. Он возвращает массив объектов PsiReference (пример).
Метод PsiReferenceProvider.getReferencesByElement() должен возвратить список ссылок (PsiReference), которые содержатся в переданном ему элементе PsiElement. В данном случае возвращается только одна ссылка, но в общем случае их может быть несколько, при этом каждая ссылка должна будет содержать соответствующий textRange (начальный индекс и конечный индекс нахождения ссылки внутри текста PSI-элемента).

Reference Contributor должен быть зарегистрирован в файле plugin.xml в соответствующей точке расширения — com.intellij.psi.referenceContributor.
После чего возможно использовать результаты его работы для получения списка ссылок при реализации метода PsiElement.getReferences(). Чтобы не дублировать этот код в каждом ссылочном элементе можно определить базовый класс для пользовательских Psi-элементов:
public class MyASTWrapperPsiElement extends ASTWrapperPsiElement {
    public MyASTWrapperPsiElement(@NotNull ASTNode astNode) {
        super(astNode);
    }

    @Override public PsiReference getReference() {
        PsiReference[] references = getReferences();
        return references.length == 0 ? null : references[0];
    }

    @NotNull @Override public PsiReference[] getReferences() {
      // Используем провайдеры, зарегистрированные в Contributor
        return ReferenceProvidersRegistry.getReferencesFromProviders(this);
    }
}

Автодополнение кода


Существует два основных типа автодополнения, которые могут быть использованы в пользовательском языковом плагине: простое ссылочное дополнение и дополнение на базе провайдера.

Простое автодополнение

Для заполнения списка дополнения, IDEA вызывает метод PsiReference.getVariants() либо у ссылки под курсором, либо у фиктивного элемента, который помещается под курсором. Этот метод должен возвратить массив объектов, содержащий строки, PSI-элементы, либо экземпляры класса LookupElement. Если в возвращенном массиве обнаружен экземпляр PsiElement, то в списке дополнения отобразится соответствующая ему иконка.
Наиболее распространенный способ реализации getVariants() является использование той же функции обхода дерева, что и в методе PsiReference.resolve(), но возвращающей все найденные определения.

Автодополнение на базе провайдера

Реализация дополнения на базе интерфейса CompletionContributor дает наибольший контроль над операцией автодополнения кода.
Основной сценарий использования Completion Contributor состоит из вызова метода extend() и передачи в параметр «pattern» соответствующего контекста, в котором применим данный вариант дополнения, в параметр «provider» передается провайдер дополнений, который генерирует соответствующие пункты списка автодополнения.

Пример: CompletionContributor для автодополнения ключевых слов в файлах MANIFEST.MF.

Пункты в списке автодополнения представлены экземплярами интерфейса LookupElement. Эти объекты обычно создаются с помощью LookupElementBuilder. Для каждого из них можно определить следующие атрибуты:
  • основной текст, дополнительный текст, строка с типом – дополнительный текст показывается сразу за основным, но не используется для поиска совпадений, предназначен в основном для показа списка параметров метода. Строка, содержащая тип дополняемого выражения, в списке дополнений выравнена по правому краю, обычно показывает возвращаемый тип метода или содержащий его класс;
  • иконку;
  • атрибуты текста;
  • обработчик при вставке текста – это метод обратного вызова, исполняемый, когда выбран какой-либо пункт из списка дополнения, может быть использован, например, для вставки скобок после вызова метода.

Поиск использований


Действие «Find Usages» в IDEA – это многошаговый процесс, каждый шаг которого требует участия со стороны плагина: в виде реализации и регистрации FindUsagesProvider в точке расширения com.intellij.lang.findUsagesProvider, а также особенностей реализации программной структуры (интерфейсы PsiNamedElement и PsiReference).

Пример: реализация FindUsagesProvider для Properties.

Для реализации данной функции должны быть выполнены следующие шаги:
  • до того как выполнить действие «Find Usages», IDEA строит индекс слов, представленных в каждом файле пользовательского языка. Используя реализацию WordsScanner, полученную от FindUsagesProvider.getWordsScanner(), IDEA загружает содержимое каждого файла и передает его в сканер слов, вместе с обработчиком слов. Сканер разбивает текст на слова, определяет контекст каждого слова (код, комментарии, строки) и передает их обработчику. Простейший путь реализации сканера – использование класса DefaultWordsScanner;
  • когда пользователь вызывает действие «Find Usages», IDEA определяет элемент, ссылки на который требуется найти. PSI-элемент под курсором (или прямой родитель в дереве токена под курсором) должен быть PsiNamedElement или ссылкой на таковой. IDEA будет использовать кэш слов для поиска текста возвращенного PsiNamedElement.getName(). Если текстовый диапазон PsiNamedElement включает другой текст помимо идентификатора, возвращенного getName(), метод getTextOffset() должен быть переписан так, чтобы возвращать стартовое смещение идентификатора;
  • как только элемент обнаружен, IDEA вызывает FindUsagesProvider. canFindUsagesFor(), чтобы узнать применимо ли действие к данному элементу;
  • когда пользователю показывается диалог «Find Usages», IDEA вызывает FindUsagesProvider.getType() и FindUsagesProvider.getDescriptiveName(), для того чтобы определить, как следует отображать этот элемент;
  • для каждого файла, содержащего найденные слова, IDEA строит PSI-дерево и производит рекурсивный спуск. IDEA разбивает текст каждого элемента на слова и сканирует их. Если элемент проиндексирован как идентификатор, то для каждого слова проверяется, не указывает ли оно на искомый элемент;
  • после того как все использования найдены, результаты показываются на панели использований. Текст для отображения пользователю берется из метода FindUsagesProvider.getNodeText().

Чтобы корректно отобразить название найденного элемента в заголовке панели Find Usages необходимо предоставить реализацию интерфейса ElementDescriptionProvider. Объект ElementDescriptionLocation, который передается в провайдер в этом случае должен иметь актуальный тип UsageViewLongNameLocation.

Пример: ElementDescriptionProvider для плагина Properties.

В следующей части: рефакторинги, форматирование и др.

Все статьи цикла: 1, 2, 3, 4, 5, 6, 7.
Tags:
Hubs:
+22
Comments 0
Comments Leave a comment

Articles