- PVSM.RU - https://www.pvsm.ru -
Дисклаймер: я не являюсь разработчиком из JetBrains, а жаль, и поэтому в статье и в коде могут быть и скорее всего есть неточности и ошибки.
Часть 1 [1]
Введение
Предусловия
Создание основы языкового плагина
Создание PSI-дерева
- Лексер
- Парсер
Аннотаторы
Подсветка скобок
Часть 2
Форматирование [2]
Structure view [3]
Find Usages [6]
Автодополнение [9]
Тестирование [10]
В предыдущей статье [11] был рассмотрен процесс создания каркаса языкового плагина. В качестве примера использовались известные плагины для Java [12], go [13], Frege [14]. Также приведены примеры из плагина [15] для языка Monkey [16], который я разработал пока разбирался, как все работает. Так как у меня не было целью охватить всё, то плагин охватывает некоторое ограниченное подмножество языка. Сам интерпретатор можно найти тут [17].
Документация [18], примеры в go-plugin [19], monkey [20].
Все как всегда начинается с точки расширения.
<lang.formatter language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.formatter.MonkeyFormattingModelBuilder"/>
Класс должен имплементировать интерфейс
public interface FormattingModelBuilder {
@NotNull
FormattingModel createModel(@NotNull FormattingContext formattingContext);
@Nullable
TextRange getRangeAffectingIndent(PsiFile file, int offset,
ASTNode elementAtOffset) ;
}
Наиболее важный - первый метод. Он возвращает модель форматирования, которая в свою очередить строится на блоках форматирования. Для упрощения можно воспользоваться методом FormattingModelProvider.createFormattingModelForPsiFile
Рассмотрим подробнее что такое блок форматирования. В IDEA блок форматирования представлен как интерфейс com.intellij.formatting.Block
- это некоторый диапазон текста (часто соответствующий какому-то PSI-элементу), к которому применяются правила форматирования. Блоки форматирования вкладываются друг в друга и образуют дерево.
public interface Block {
@NotNull
TextRange getTextRange();
@NotNull
List<Block> getSubBlocks();
@Nullable
Wrap getWrap();
@Nullable
Indent getIndent();
@Nullable
Alignment getAlignment();
@Nullable
Spacing getSpacing(@Nullable Block child1, @NotNull Block child2);
@NotNull
ChildAttributes getChildAttributes(final int newChildIndex);
boolean isIncomplete();
boolean isLeaf();
@Nullable
default String getDebugName() {
return null;
}
}
Для того чтобы увидеть визуализацию этого дерева, можно воспользоваться инструментом PSI-Viewer (Tools->View PSI Structure, раздел Block Sructure)
Для упрощения реализации можно воспользоваться классом AbstractBlock
. При этом вместо метода getSubBlocks
он предлагает реализовывать метод buildChildren
, который возращает список непосредственных дочерних элементов данного блока.
getSpacing
- определяет количество пробелов или перенос строк между указанными дочерними элементами. Для упрощения реализации данной логики можно воспользоваться классом com.intellij.formatting.SpacingBuilder
, который предоставляет удобное API для описания правил.
//сильно упрощенный пример из плагина plugin-go
public Spacing getSpacing(@Nullable Block child1, @NotNull Block child2) {
return new SpacingBuilder(settings, GoLanguage.INSTANCE)
.before(COMMA).spaceIf(false)
.after(COMMA).spaceIf(true)
.betweenInside(SEMICOLON, SEMICOLON, FOR_CLAUSE).spaces(1)
.before(SEMICOLON).spaceIf(false)
.after(SEMICOLON).spaceIf(true)
.beforeInside(DOT, IMPORT_SPEC).none()
.afterInside(DOT, IMPORT_SPEC).spaces(1)
//и так далее
.getSpacing(this, child1, child2);
}
Если не подходит builder, то можно создать свое описание через com.intellij.formatting.Spacing#createSpacing
getIndent
- определяет правила отступа относительно родительского блока
getWrap
- нужно ли переносить на другую строчку контент
getAlignment
- выравнивание блоков друг относительно друга
getChildIndent
- используется AbstractBlock
для вычисления getChildAttributes
, который возвращает правила для блока, создаваемого после нажатия клавиши Enter.
В этом разделе пойдет речь об наполнении вот этой панели:
За это отвечает точка расширения
<lang.psiStructureViewFactory language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.editor.MonkeyStructureViewFactory"/>
Класс должен имплементировать интерфейс:
@FunctionalInterface
public interface PsiStructureViewFactory {
@Nullable
StructureViewBuilder getStructureViewBuilder(@NotNull PsiFile psiFile);
}
Для реализации StructureViewBuilder
можно использовать уже готовые заготовки com.intellij.ide.structureView.TreeBasedStructureViewBuilder
, com.intellij.ide.structureView.StructureViewModelBase
и com.intellij.ide.structureView.impl.common.PsiTreeElementBase
.
Работа с PsiTreeElementBase
напоминает работу с блоками форматирования.
Документация [21]. Примеры реализации в plugin-go [22], Monkey [23].
Начнем с кэшей. IDEA опрашивает многие расширения по нескольку раз. Если расширение выполняет какую-то тяжелую работу, то самое лучшее решение - это закэшировать результат этой работы. IDEA предоставляет много готовых оберток для этого. Например, так вычисляется тип Go выражения в плагине plugin-go:
@Nullable
public static GoType getGoType(@NotNull GoExpression o, @Nullable ResolveState context) {
return RecursionManager.doPreventingRecursion(o, true, () -> {
if (context != null) return unwrapParType(o, context);
return CachedValuesManager.getCachedValue(o, () -> CachedValueProvider.Result
.create(unwrapParType(o, createContextOnElement(o)), PsiModificationTracker.MODIFICATION_COUNT));
});
}
Здесь используется 2 менеджера. Первый - CachedValuesManager
, который кэширует результат для psi-элемента, и второй - RecursionManager
, который помогает предотвращать бесконечную рекурсию и StackOverflowError
. Также есть специализированный кэш com.intellij.psi.impl.source.resolve.ResolveCache
, который используется при разрешении (resolving) элементов (про это ниже).
Документация [24]. Все мы знаем как IDEA любит всё индексировать. Давайте посмотрим, что это такое и как это можно использовать.
Индексы в IDEA предоставляют возможность быстро находить файл с привязанной другой информацией или psi-элемент по выбранному ключу (например, по названию аннотации можно найти все места, где она используется).
Посмотреть существующие индексы можно с помощью плагина Index viewer [25]. О том, как все работает, также можно почитать в статье про Frege [26].
IDEA поддерживает два вида индексов - File-based и Stub. File-based работает с содержимым файла, Stub работает с Stub-деревом, которое строится на базе Psi-дерева.
Пример использования можно посмотреть в плагине haskforce [27]
Начинается все как всегда с подключения точки расширения
<fileBasedIndex implementation="com.haskforce.index.HaskellModuleIndex"/>
Файл должен расширять FileBasedIndexExtension
. Получить результаты индексирования можно через
FileBasedIndex.getInstance()
Плагин haskforce [27], например, использует этот тип индекса, чтобы получить все файлы в рамках модуля:
@NotNull
public static Collection<VirtualFile> getVirtualFilesByModuleName(@NotNull String moduleName, @NotNull GlobalSearchScope searchScope) {
return FileBasedIndex.getInstance()
.getContainingFiles(HASKELL_MODULE_INDEX, moduleName, searchScope);
}
Этот тип индексов, как мне кажется, используется чаще, так как позволяет искать по PSI-элементам (а точнее по stub, который отображает требуемую часть psi-дерева). Stub используются только для именнованных psi-элементов (которые имплементируют интерфейс PsiNamedElement). О них будет подробнее написано в разделе Reference
Чтобы объявить новый индекс, используется следующая точка расширения:
<stubIndex implementation=
"com.github.pyltsin.monkeyplugin.stubs.MonkeyVarNameIndex"/>
Класс должен имплементировать StubIndexExtension
(или его наследников, например, StringStubIndexExtension
).
Пример из плагина для Monkey:
class MonkeyVarNameIndex : StringStubIndexExtension<MonkeyNamedElement>() {
override fun getVersion(): Int {
return super.getVersion() + VERSION
}
override fun getKey(): StubIndexKey<String, MonkeyNamedElement> {
return KEY
}
companion object {
val KEY: StubIndexKey<String, MonkeyNamedElement> =
StubIndexKey.createIndexKey("monkey.var.name")
const val VERSION = 0
}
}
Примеры в go-plugin [28], frege [29].
Теперь нам нужно научить IDEA создавать дерево Stub-ов и записывать необходимые элементы под нужным индексом.
Для каждого типа элементов, который мы хотим сохранить как Stub, мы создаем определение Stub. При этом корнем всех Stub-ов должен быть FileStub.
Пример из плагина для Monkey:
class MonkeyFileStub(file: MonkeyFile?) : PsiFileStubImpl<MonkeyFile>(file)
class MonkeyVarDefinitionStub : NamedStubBase<MonkeyVarDefinition> {
constructor(parent: StubElement<*>?, elementType: IStubElementType<*, *>, name: StringRef?) : super(
parent,
elementType,
name
)
constructor(parent: StubElement<*>?, elementType: IStubElementType<*, *>, name: String?) : super(
parent,
elementType,
name
)
}
Примеры из go-plugin (файл [30], элемент [31]), Frege (файл [32], элемент [33])
Следующий шаг - для каждого Stub создать описание типа элемента этого Stub. (Для автоматически генерируемых PSI-элементов с помощь Grammar-Kit плагина описания каждого типа элемента создается автоматически в соответствии с параметрами elementTypeHolderClass
и elementTypeClass
). ElementType для файла должен расширять IStubFileElementType
, для элемента - IStubElementType
.
IStubElementType
требует реализации следующих методов:
@NotNull
String getExternalId();
void serialize(@NotNull T stub, @NotNull StubOutputStream dataStream) throws IOException;
@NotNull
T deserialize(@NotNull StubInputStream dataStream, P parentStub) throws IOException;
void indexStub(@NotNull T stub, @NotNull IndexSink sink);
PsiT createPsi(@NotNull StubT stub);
@NotNull StubT createStub(@NotNull PsiT psi, StubElement<?> parentStub);
shouldCreateStub(ASTNode node)
Как индексировать Stub указывается в методе indexStub
. Например, в Monkey я использовал такую реализацию:
override fun indexStub(stub: S, sink: IndexSink) {
val name = stub.name
if (name != null) {
sink.occurrence(MonkeyVarNameIndex.KEY, name)
}
}
Реализацию других методов можно посмотреть в примерах - плагин [34] для Monkey, go-plugin [35], Frege [36]
Теперь наши stub-ы надо подключить к парсеру. Это делается в 2 шага.
1 шаг: определить свою фабрику типов элементов, для которых мы сделали IStubElementType
, например как
object MonkeyElementTypeFactory {
@JvmStatic
fun factory(name: String): IElementType {
if (name == "VAR_DEFINITION") return MonkeyVarDefinitionStubElementType(name)
throw RuntimeException("Unknown element type: $name")
}
}
и указать в bnf файле, что теперь ее надо использовать для некоторых PSI-элементов:
elementTypeFactory("var_definition")=
"com.github.pyltsin.monkeyplugin.psi.impl.MonkeyElementTypeFactory.factory"
2 шаг. Указать, что эти PSI-элементы должны расширять StubBasedPsiElementBase
Рассмотрим все вместе. В процессе индексации создается StubBasedPsiElementBase
, затем с помощью IStubElementType.createStub создается Stub, который сериализуется (serialize
) и ссылка на него сохраняется в индекс(indexStub
).
Клиентский код, который работает со Stub, полученными из индексов, должен вызывать только те методы, для выполнения которых достаточно сохраненной информации. Поэтому в stub надо включать всю информацию, которая может понадобится в дальнейшем при анализе. Чтобы получить PSI-элемент, можно вызвать метод getNode(), но он дорогой, так как требует парсинга файла.
Пример сохранения информации можно посмотреть в com.intellij.psi.impl.java.stubs.impl.PsiAnnotationStubImpl#getPsiElement
, в котором используется текст из ASTNode.
Индексы широко используются для go-to функций. Например, для работы данной панели:
plugin.xml:
<gotoSymbolContributor implementation=
"com.github.pyltsin.monkeyplugin.usages.MonkeySymbolContributor"/>
MonkeySymbolContributor:
class MonkeySymbolContributor : ChooseByNameContributorEx {
private val myIndexKeys = listOf(MonkeyVarNameIndex.KEY)
override fun processNames(
processor: Processor<in String>,
scope: GlobalSearchScope,
filter: IdFilter?
) {
for (key in myIndexKeys) {
ProgressManager.checkCanceled()
StubIndex.getInstance().processAllKeys(
key,
processor,
scope,
filter
)
}
}
override fun processElementsWithName(
name: String,
processor: Processor<in NavigationItem>,
parameters: FindSymbolParameters
) {
for (key in myIndexKeys) {
ProgressManager.checkCanceled()
StubIndex.getInstance().processElements(
key,
name,
parameters.project,
parameters.searchScope,
parameters.idFilter,
MonkeyNamedElement::class.java,
processor
)
}
}
}
Многие плагины также используют индексы. Например, Request mapper [37] (на данный момент не поддерживается, так как такая же функциональность появилась в IDEA), который помогает искать точки объявления REST-методов
Под капотом RequestMapper использует такой код:
//JavaAnnotationIndex - обычный Stub index для Java для аннотаций
JavaAnnotationIndex
.getInstance()
.get(annotationName, project, GlobalSearchScope.projectScope(project))
.asSequence()
Документация [38]
На данный момент идет изменение API в этой части, поэтому могут быть неточности.
Ссылки создают связи между элементами, так, нажав Ctrl+B, вы перейдете на тот элемент, на который эта ссылка ссылается. Нажав Ctrl+B еще раз, вы увидите все элементы, которые ссылаются на этот элемент.
Элемент, который ссылается, и элемент, на который ссылаются, также могут иметь имя. Только тот элемент, который определяет это имя, должен реализовывать PsiNamedElement
(а лучше PsiNameIdentifierOwner
, их использование можно посмотреть в разделе Rename)
@JvmStatic
fun setName(expr: MonkeySimpleRefExpr, name: String): PsiElement {
val e: PsiElement = MonkeyElementTextFactory.createStatementFromText(expr.project, "$name + 1")
//newLetExpr должен реализовывать PsiNamedElement
val newLetExpr = PsiTreeUtil.findChildOfType(e, MonkeySimpleRefExpr::class.java)
if (newLetExpr != null) {
expr.replace(newLetExpr) //newLetExpr должен предоставлять ссылку
}
return expr
}
Для того чтобы PSI-элемент мог предоставить ссылку, нужно реализовать методы
PsiReference getReference();
//если элемент ссылается на несколько элементов, то
PsiReference @NotNull [] getReferences();
//или новый метод
@Experimental
default @NotNull Iterable<? extends @NotNull PsiSymbolReference> getOwnReferences() {
return Collections.emptyList();
}
Для упрощения реализации интерфейса PsiReference можно воспользоваться заготовкой PsiReferenceBase. Остается реализовать метод PsiElement resolve()
илиResolveResult [] multiResolve(boolean incompleteCode)
, которые возвращают элементы, на которые ссылаются. При реализации этого метода имеет смысл использовать специализированный кэш:
override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
return ResolveCache.getInstance(psiElement.project).resolveWithCaching(
this, { referenceBase, _ ->
referenceBase.resolveInner(false)
.map { PsiElementResolveResult(it) }
.toTypedArray()
},
true, false
)
}
После реализации данной части IDEA уже сможет предоставлять навигацию к элементу, на который ссылаются и обратно.
Документация [39]. Примеры go-plugin [40], monkey [41].
Предоставляет возможность из этого меню:
Точка расширения:
<lang.findUsagesProvider language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.usages.MonkeyFindUsagesProvider"/>
Класс должен реализовать интерфейс FindUsagesProvider
.
Документация [42]
Одним из самых популярных рефакторингов является переименование (Shift+F6)
За это отвечают две точки расширения:
<lang.refactoringSupport language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.refactor.MonkeyRefactoringSupportProvider"/>
<renameInputValidator implementation="com.github.pyltsin.monkeyplugin.refactor.MonkeyRenameInputValidator"/>
renameInputValidator
должен реализовывать интерфейс RenameInputValidator
refactoringSupport
должен расширять абстрактный класс RefactoringSupportProvider
. Данный класс также содержит методы для поддержки других видов рефакторингов. На данный момент нас интересует метод, который указывает, поддерживается ли редактирование на месте.
public boolean isMemberInplaceRenameAvailable(@NotNull PsiElement element, @Nullable PsiElement context) {
return false;
}
Теперь требуется реализовать методы переименования. Как уже было сказано до этого, PSI-элемент, который определяет имя, следует реализовывать интерфейс PsiNamedElement
.
public interface PsiNamedElement extends PsiElement {
String getName();
PsiElement setName(@NlsSafe @NotNull String name)
throws IncorrectOperationException;
}
Нас здесь интересует метод setName
. Одним из самых простых способов реализовать этот метод - это создать новый PSI-элемент из текста, например так
private fun createFileFromText(project: Project, text: String): MonkeyFile {
return PsiFileFactory.getInstance(project)
.createFileFromText("A.monkey", MonkeyLanguage.INSTANCE, text) as MonkeyFile
}
И заменить элемент
expr.replace(newLetExpr)
Осталось реализовать переименование элементов, которые ссылаются на наш именованный PSI-элемент. Для этого нужно реализовать метод из PsiReference
PsiElement handleElementRename(@NotNull String newElementName)
throws IncorrectOperationException;
или воспользоваться готовой реализацией из com.intellij.psi.PsiReferenceBase
, если она подходит.
IDEA активно использует подсказки в виде маркеров
Примеры go-plugin [43], Frege [44], monkey [45]
Точка расширения:
<codeInsight.lineMarkerProvider language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.editor.MonkeyFunctionLineMarkerProvider"/>
Файл должен реализовывать интерфейс LineMarkerProvider
, методы которого возвращают объект LineMarkerInfo
. Обратите внимание, что маркеры следует привязывать только к листьям PSI-дерева
Думаю, все кто работает с IDEA любит, как она работает с подсказками (хотя, конечно, есть случаи, когда она немного тупит). Написать хороший механизм автодополнения очень сложно. Был даже цикл статье про это [46]. Но при этом можно относительно быстро реализовать некоторые подсказки, например, вот такие:
Это делается через реализацию метода PsiReference.getVariants
, который должен возвращать все видимые подходящие значения. Фильтрацию по символам выполняет сама IDEA.
Для более сложных кейсов можно воспользоваться точкой расширения:
<completion.contributor language="Monkey"
implementationClass="com.github.pyltsin.monkeyplugin.completion.MonkeyKeywordCompletionContributor"/>
Класс должен реализовывать абстрактный класс CompletionContributor
.
Документация [47]. Примеры реализации go-plugin [48], frege [49], monkey [50] (начальная поддержка автодополнения ключевых слов)
Документация [51]. Тестирование frege [52]
В IDEA тесты часто представляют собой несколько файлов с исходным кодом языка, которые показывают состояние ДО и ПОСЛЕ действия. Для тестов обычно используется класс BasePlatformTestCase
, а сами тесты выглядят как-то так:
myFixture.configureByFiles("RenameTestData.monkey")
myFixture.renameElementAtCaret("test")
myFixture.checkResultByFile("RenameTestData.monkey", "RenameTestDataAfter.monkey", false)
где RenameTestData.monkey и RenameTestDataAfter.monkey - файлы с исходным кодом до и после переименования.
На этом рассказ о создании языкового плагина для IDEA закончен. Удачи в настройке своего IDE под себя!
Автор:
pyltsinm
Источник [53]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/372814
Ссылки в тексте:
[1] Часть 1: https://habr.com/ru/post/653183/
[2] Форматирование: #format
[3] Structure view: #structureview
[4] Кэши, индексы, stub и goto: #cache
[5] Ссылки (Reference): #reference
[6] Find Usages: #usages
[7] Rename и другие рефакторинги: #rename
[8] Маркеры (Markers): #notes
[9] Автодополнение: #completion
[10] Тестирование: #test
[11] статье: https://habr.com/ru/publication/edit/653183/
[12] Java: https://github.com/JetBrains/intellij-community
[13] go: https://github.com/go-lang-plugin-org/go-lang-idea-plugin
[14] Frege: https://github.com/IntelliJ-Frege
[15] плагина: https://github.com/pyltsin/monkey-plugin
[16] Monkey: https://monkeylang.org/
[17] тут: https://github.com/pyltsin/monkey-source
[18] Документация: https://plugins.jetbrains.com/docs/intellij/code-formatting.html
[19] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/tree/master/src/com/goide/formatter
[20] monkey: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/formatter/MonkeyFormattingModelBuilder.kt
[21] Документация: https://plugins.jetbrains.com/docs/intellij/structure-view.html
[22] plugin-go: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/tree/GoStructureViewFactory.java
[23] Monkey: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/editor/MonkeyStructureViewFactory.kt
[24] Документация: https://plugins.jetbrains.com/docs/intellij/indexing-and-psi-stubs.html#indexes
[25] Index viewer: https://plugins.jetbrains.com/plugin/13029-index-viewer
[26] Frege: https://habr.com/ru/company/hsespb/blog/574692/
[27] haskforce: https://github.com/carymrobbins/intellij-haskforce
[28] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/stubs/index/GoAllPublicNamesIndex.java
[29] frege: https://github.com/IntelliJ-Frege/intellij-frege/tree/master/src/main/kotlin/com/plugin/frege/stubs/index
[30] файл: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/stubs/GoFileStub.java
[31] элемент: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/stubs/GoFieldDefinitionStub.java
[32] файл: https://github.com/IntelliJ-Frege/intellij-frege/blob/master/src/main/kotlin/com/plugin/frege/stubs/FregeFileStub.kt
[33] элемент: https://github.com/IntelliJ-Frege/intellij-frege/blob/master/src/main/kotlin/com/plugin/frege/stubs/FregeClassStub.kt
[34] плагин: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/stubs/types/MonkeyVarDefinitionStubElementType.kt
[35] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/stubs/types/GoMethodDeclarationStubElementType.java
[36] Frege: https://github.com/IntelliJ-Frege/intellij-frege/blob/master/src/main/kotlin/com/plugin/frege/stubs/types/FregeMethodElementType.kt
[37] Request mapper: https://plugins.jetbrains.com/plugin/9567-request-mapper
[38] Документация: https://plugins.jetbrains.com/docs/intellij/declarations-and-references.html
[39] Документация: https://plugins.jetbrains.com/docs/intellij/find-usages.html
[40] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/usages/GoFindUsagesProvider.java
[41] monkey: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/usages/MonkeyFindUsagesProvider.kt
[42] Документация: https://plugins.jetbrains.com/docs/intellij/rename-refactoring.html
[43] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/marker/GoRecursiveCallMarkerProvider.java
[44] Frege: https://github.com/IntelliJ-Frege/intellij-frege/blob/master/src/main/kotlin/com/plugin/frege/linemarker/related/FregeRelatedItemLineMarkerProvider.kt
[45] monkey: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/editor/MonkeyFunctionLineMarkerProvider.kt
[46] это: https://habr.com/ru/company/JetBrains/blog/568884/
[47] Документация: https://plugins.jetbrains.com/docs/intellij/code-completion.html
[48] go-plugin: https://github.com/go-lang-plugin-org/go-lang-idea-plugin/blob/master/src/com/goide/completion/GoKeywordCompletionContributor.java
[49] frege: https://github.com/IntelliJ-Frege/intellij-frege/blob/master/src/main/kotlin/com/plugin/frege/completion/FregeKeywordCompletionContributor.kt
[50] monkey: https://github.com/pyltsin/monkey-plugin/blob/main/src/main/kotlin/com/github/pyltsin/monkeyplugin/completion/MonkeyKeywordCompletionContributor.kt
[51] Документация: https://plugins.jetbrains.com/docs/intellij/writing-tests-for-plugins.html
[52] Тестирование frege: https://habr.com/ru/company/hsespb/blog/575566/
[53] Источник: https://habr.com/ru/post/653667/?utm_source=habrahabr&utm_medium=rss&utm_campaign=653667
Нажмите здесь для печати.