Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2025 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.platform.bukkit.completion

import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiClass

class BukkitEventHandlerCompletionContributor : CompletionContributor() {
init {
extend(
CompletionType.BASIC,
PlatformPatterns.psiElement().inside(PsiClass::class.java),
BukkitEventHandlerCompletionProvider()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2025 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.platform.bukkit.completion

import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants
import com.demonwav.mcdev.util.findContainingClass
import com.demonwav.mcdev.util.findContainingMethod
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.impl.JavaPsiFacadeEx
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ProcessingContext
import org.jetbrains.annotations.NotNull

class BukkitEventHandlerCompletionProvider : CompletionProvider<CompletionParameters>() {

override fun addCompletions(
completionParameters: CompletionParameters,
processingContext: ProcessingContext,
completionResultSet: CompletionResultSet
) {
val prefix = completionResultSet.prefixMatcher.prefix
if (!prefix.startsWith("on") || prefix.length == 2) {
return
}

val position = completionParameters.position
val containingClass = position.findContainingClass() ?: return
val project = position.project
val facade = JavaPsiFacadeEx.getInstance(project)

val eventListenerClass = facade.findClass(BukkitConstants.LISTENER_CLASS, GlobalSearchScope.allScope(project)) ?: return
if (!containingClass.isInheritor(eventListenerClass, true)) {
return
}

if (position.findContainingMethod() != null) {
return
}

val scope = GlobalSearchScope.allScope(project)
val eventBaseClass = facade.findClass(BukkitConstants.EVENT_CLASS, scope) ?: return
val eventNameFilter = prefix.substring(2).lowercase()

ClassInheritorsSearch.search(eventBaseClass, scope, true)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks expensive. However it's not so easy to cache effectively because the easiest way to do that is using the PSI modification count as the cache key, and since you're in a completion provider that is changing on every keystroke. Leave it as it is for now.

.forEach { psiClass ->
if (psiClass.isInterface || psiClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
return@forEach
}

val eventSimpleName = psiClass.name ?: return@forEach
if (eventNameFilter.isNotEmpty() && !eventSimpleName.lowercase().startsWith(eventNameFilter)) {
return@forEach
}

val lookupString = "on$eventSimpleName"
val methodName = lookupString.removeSuffix("Event")
val qualifiedName = psiClass.qualifiedName

val element = LookupElementBuilder
.create(lookupString)
.withPresentableText("$lookupString()")
.withTailText(" - $qualifiedName", true)
.withTypeText("@EventHandler")
.withIcon(AllIcons.Nodes.Method)
.withBaseLookupString(lookupString)
.withBoldness(true)
.withInsertHandler(BukkitEventHandlerInsertHandler(methodName, qualifiedName))

completionResultSet.addElement(
PrioritizedLookupElement.withPriority(element, 100.0)
)
}

completionResultSet.stopHere()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2025 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.platform.bukkit.completion

import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.NotNull

class BukkitEventHandlerInsertHandler(
private val methodName: String,
private val qualifiedName: @NlsSafe String?
) : InsertHandler<LookupElement> {

override fun handleInsert(insertionContext: InsertionContext, lookupElement: LookupElement) {
val project = insertionContext.project
val editor = insertionContext.editor

val templateManager = TemplateManager.getInstance(project)
val template = templateManager.createTemplate("", "")
template.isToReformat = true

template.addTextSegment("@${BukkitConstants.HANDLER_ANNOTATION}\n")
template.addTextSegment("public void ")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the format of this template is hard-coded. Some people might want a different format, like have their methods be package-private (like I do). Some people might also want a different default method name (I've seen people put on as every handler's method name), or even different default event name. Perhaps it could be beneficial to expose properly editing the template in some way? IntelliJ already provides an interface for users to edit templates for these things, so perhaps you could hook into that and provide the method names/event params as built-in variables instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will read into it thanks for making me aware of this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did play around a bit but I don't think i can implement this in a clean way. Especially with using existing logic.

It definitely should be possible but I simply am not familiar enough with kotlin to do it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might play around with this and send a patch if I succeed, no guarantees though.

template.addVariable("METHOD_NAME", TextExpression(methodName), true)
template.addTextSegment("($qualifiedName ")
template.addVariable("EVENT_PARAM", TextExpression("event"), true)
template.addTextSegment(") { \n")
template.addEndVariable()
template.addTextSegment("\n}")

insertionContext.document.deleteString(
insertionContext.startOffset,
insertionContext.tailOffset
)

templateManager.startTemplate(editor, template)

}
}
7 changes: 7 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,13 @@
<library.presentationProvider
implementation="com.demonwav.mcdev.platform.bukkit.framework.ModernPaperPresentationProvider"/>

<!-- Bukkit Completion Contributor -->

<completion.contributor
language="JAVA"
implementationClass="com.demonwav.mcdev.platform.bukkit.completion.BukkitEventHandlerCompletionContributor"
order="first"/>

<!--endregion-->

<!--region SPONGE-->
Expand Down