Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Vaadin Endpoints integration #167

Merged
merged 5 commits into from
Nov 21, 2024
Merged
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
9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ repositories {

dependencies {
intellijPlatform {
create("IC", "2023.3")
create("IU", "2023.3")
Copy link
Member Author

Choose a reason for hiding this comment

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

Ultimate here is for build purpose. Ultimate dependencies are marked as optional in plugin.xml

bundledPlugin("com.intellij.java")
bundledPlugin("org.jetbrains.idea.maven")
bundledPlugin("org.jetbrains.plugins.gradle")
bundledPlugin("com.intellij.properties")
bundledPlugin("com.intellij.microservices.jvm")

pluginVerifier()
zipSigner()
Expand Down Expand Up @@ -78,7 +80,10 @@ configure<com.diffplug.gradle.spotless.SpotlessExtension> {
}

tasks {
patchPluginXml { sinceBuild.set("233") }
patchPluginXml {
sinceBuild.set("233")
untilBuild.set("252.*")
Copy link
Member Author

Choose a reason for hiding this comment

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

Required for running 1.0-SNAPSHOT version locally

}

signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.vaadin.plugin.endpoints

import com.intellij.microservices.endpoints.*
import com.intellij.microservices.endpoints.EndpointsProvider.Status
import com.intellij.microservices.endpoints.presentation.HttpUrlPresentation
import com.intellij.microservices.url.UrlPath
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ModificationTracker
import com.intellij.psi.PsiElement
import com.intellij.uast.UastModificationTracker
import com.vaadin.plugin.utils.VaadinIcons

internal class VaadinEndpointsProvider : EndpointsProvider<VaadinRoute, VaadinRoute> {
override val endpointType: EndpointType = HTTP_SERVER_TYPE

override val presentation: FrameworkPresentation =
FrameworkPresentation("Vaadin", "Vaadin Flow", VaadinIcons.VAADIN_BLUE)

override fun getStatus(project: Project): Status {
if (hasVaadinFlow(project)) return Status.HAS_ENDPOINTS

return Status.UNAVAILABLE
}

override fun getModificationTracker(project: Project): ModificationTracker {
return UastModificationTracker.getInstance(project)
}

override fun getEndpointGroups(project: Project, filter: EndpointsFilter): Iterable<VaadinRoute> {
if (filter !is ModuleEndpointsFilter) return emptyList()
if (!hasVaadinFlow(filter.module)) return emptyList()

return findVaadinRoutes(project, filter.transitiveSearchScope)
}

override fun getEndpoints(group: VaadinRoute): Iterable<VaadinRoute> {
return listOf(group)
}

override fun isValidEndpoint(group: VaadinRoute, endpoint: VaadinRoute): Boolean {
return group.isValid()
}

override fun getEndpointPresentation(group: VaadinRoute, endpoint: VaadinRoute): ItemPresentation {
return HttpUrlPresentation(normalizeUrl(group.urlMapping), group.locationString, VaadinIcons.VAADIN_BLUE)
}

private fun normalizeUrl(urlMapping: String): String {
val urlString = run {
if (urlMapping.isBlank()) return@run "/"
if (!urlMapping.startsWith("/")) return@run "/$urlMapping"
return@run urlMapping
}

return parseVaadinUrlMapping(urlString).getPresentation(VaadinUrlRenderer)
}

override fun getDocumentationElement(group: VaadinRoute, endpoint: VaadinRoute): PsiElement? {
return endpoint.anchor.retrieve()
}
}

private object VaadinUrlRenderer : UrlPath.PathSegmentRenderer {
override fun visitVariable(variable: UrlPath.PathSegment.Variable): String {
return "{${variable.variableName}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.vaadin.plugin.endpoints

import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInsight.daemon.ImplicitUsageProvider
import com.intellij.lang.jvm.JvmModifier
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.util.InheritanceUtil

internal class VaadinImplicitUsageProvider : ImplicitUsageProvider {
override fun isImplicitUsage(element: PsiElement): Boolean {
return element is PsiClass &&
!element.isInterface &&
!element.isEnum &&
!element.hasModifier(JvmModifier.ABSTRACT) &&
!element.isAnnotationType &&
(AnnotationUtil.isAnnotated(element, VAADIN_ROUTE, 0) ||
AnnotationUtil.isAnnotated(element, VAADIN_TAG, 0) ||
InheritanceUtil.isInheritor(element, VAADIN_APP_SHELL_CONFIGURATOR))
}

override fun isImplicitRead(element: PsiElement): Boolean {
return false
}

override fun isImplicitWrite(element: PsiElement): Boolean {
return element is PsiField && AnnotationUtil.isAnnotated(element, VAADIN_ID, 0)
}
}
50 changes: 50 additions & 0 deletions src/main/kotlin/com/vaadin/plugin/endpoints/VaadinModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.vaadin.plugin.endpoints

import com.intellij.java.library.JavaLibraryUtil.hasLibraryClass
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnchor
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import com.intellij.psi.search.searches.AnnotatedElementsSearch
import com.intellij.util.Processor
import org.jetbrains.uast.UClass
import org.jetbrains.uast.evaluateString
import org.jetbrains.uast.toUElementOfType

internal const val VAADIN_ROUTE = "com.vaadin.flow.router.Route"
internal const val VAADIN_APP_SHELL_CONFIGURATOR = "com.vaadin.flow.component.page.AppShellConfigurator"
internal const val VAADIN_ID = "com.vaadin.flow.component.template.Id"
internal const val VAADIN_TAG = "com.vaadin.flow.component.Tag"

internal fun hasVaadinFlow(project: Project): Boolean = hasLibraryClass(project, VAADIN_ROUTE)

internal fun hasVaadinFlow(module: Module): Boolean = hasLibraryClass(module, VAADIN_ROUTE)

internal fun findVaadinRoutes(project: Project, scope: GlobalSearchScope): Collection<VaadinRoute> {
val vaadinRouteClass =
JavaPsiFacade.getInstance(project).findClass(VAADIN_ROUTE, ProjectScope.getLibrariesScope(project))
?: return emptyList()

val routes = ArrayList<VaadinRoute>()

AnnotatedElementsSearch.searchPsiClasses(vaadinRouteClass, scope)
.forEach(
Processor { psiClass ->
val uClass = psiClass.toUElementOfType<UClass>()
val sourcePsi = uClass?.sourcePsi
val className = psiClass.name

if (sourcePsi == null || className == null) return@Processor true
val uAnnotation = uClass.findAnnotation(VAADIN_ROUTE) ?: return@Processor true

val urlMapping = uAnnotation.findAttributeValue("value")?.evaluateString() ?: ""

routes.add(VaadinRoute(urlMapping, className, PsiAnchor.create(sourcePsi)))

true
})

return routes.toList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.vaadin.plugin.endpoints

import com.intellij.codeInsight.highlighting.HighlightedReference
import com.intellij.lang.properties.references.PropertyReference
import com.intellij.microservices.jvm.url.uastUrlPathReferenceInjectorForScheme
import com.intellij.microservices.jvm.url.uastUrlReferenceProvider
import com.intellij.microservices.url.HTTP_SCHEMES
import com.intellij.patterns.PsiJavaPatterns.psiMethod
import com.intellij.patterns.uast.callExpression
import com.intellij.patterns.uast.injectionHostUExpression
import com.intellij.psi.*
import com.intellij.util.ProcessingContext
import org.jetbrains.uast.UElement
import org.jetbrains.uast.expressions.UInjectionHost

internal class VaadinReferenceContributor : PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerUastReferenceProvider(
injectionHostUExpression().annotationParam(VAADIN_ROUTE, "value"),
uastUrlReferenceProvider(uastUrlPathReferenceInjectorForScheme(HTTP_SCHEMES, vaadinUrlPksParser)))

registrar.registerUastReferenceProvider(
injectionHostUExpression()
.callParameter(
0,
callExpression()
.withMethodName("getTranslation")
.withAnyResolvedMethod(
psiMethod()
.withName("getTranslation")
.definedInClass("com.vaadin.flow.component.Component"))),
object : UastReferenceProvider() {
override fun getReferencesByElement(
element: UElement,
context: ProcessingContext
): Array<PsiReference> {
if (element !is UInjectionHost) return PsiReference.EMPTY_ARRAY
val key = element.evaluateToString() ?: return PsiReference.EMPTY_ARRAY
val sourcePsi = element.sourcePsi ?: return PsiReference.EMPTY_ARRAY

return arrayOf(object : PropertyReference(key, sourcePsi, null, false), HighlightedReference {})
}
})
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/vaadin/plugin/endpoints/VaadinRoute.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vaadin.plugin.endpoints

import com.intellij.psi.PsiAnchor

internal class VaadinRoute(val urlMapping: String, val locationString: String, val anchor: PsiAnchor) {
fun isValid(): Boolean = anchor.retrieve() != null
}
90 changes: 90 additions & 0 deletions src/main/kotlin/com/vaadin/plugin/endpoints/VaadinUrlResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.vaadin.plugin.endpoints

import com.intellij.microservices.jvm.cache.ModuleCacheValueHolder
import com.intellij.microservices.jvm.cache.SourceLibSearchProvider
import com.intellij.microservices.jvm.cache.UastCachedSearchUtils.sequenceWithCache
import com.intellij.microservices.url.*
import com.intellij.microservices.url.UrlPath.PathSegment
import com.intellij.microservices.url.references.UrlPksParser
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiAnchor
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PartiallyKnownString
import com.intellij.psi.util.SplitEscaper
import com.vaadin.plugin.utils.VaadinIcons
import javax.swing.Icon

internal class VaadinUrlResolverFactory : UrlResolverFactory {
override fun forProject(project: Project): UrlResolver? {
return if (hasVaadinFlow(project)) VaadinUrlResolver(project) else null
}
}

internal class VaadinUrlResolver(private val project: Project) : UrlResolver {
override val supportedSchemes: List<String>
get() = HTTP_SCHEMES

override fun getVariants(): Iterable<UrlTargetInfo> {
return getAllModuleVariants(project).asIterable()
}

override fun resolve(request: UrlResolveRequest): Iterable<UrlTargetInfo> {
if (request.method != HttpMethods.GET) return emptyList()

val allModuleVariants = getAllModuleVariants(project).toList()

return UrlPath.combinations(request.path)
.flatMap { path -> allModuleVariants.asSequence().filter { it.path.isCompatibleWith(path) } }
.asIterable()
}
}

internal val VAADIN_ROUTES_SEARCH: SourceLibSearchProvider<List<VaadinRoute>, Module> =
SourceLibSearchProvider("VAADIN_ROUTES") { p, _, scope -> findVaadinRoutes(p, scope).toList() }

private fun getAllModuleVariants(project: Project): Sequence<VaadinUrlTargetInfo> {
val modules = ModuleManager.getInstance(project).modules

return modules.asSequence().flatMap(::getVariants).map(::VaadinUrlTargetInfo)
}

private fun getVariants(module: Module): Sequence<VaadinRoute> {
if (!hasVaadinFlow(module)) return emptySequence()

return sequenceWithCache(ModuleCacheValueHolder(module), VAADIN_ROUTES_SEARCH)
}

private class VaadinUrlTargetInfo(route: VaadinRoute) : UrlTargetInfo {
private val anchor: PsiAnchor = route.anchor

override val authorities: List<Authority>
get() = emptyList()

override val path: UrlPath = parseVaadinUrlMapping(route.urlMapping)

override val icon: Icon
get() = VaadinIcons.VAADIN_BLUE

override val schemes: List<String>
get() = HTTP_SCHEMES

override fun resolveToPsiElement(): PsiElement? = anchor.retrieve()
}

internal val vaadinUrlPksParser: UrlPksParser =
UrlPksParser(
splitEscaper = { _, _ -> SplitEscaper.AcceptAll },
customPathSegmentExtractor = { part ->
if (part.startsWith(":")) {
val varName = part.removePrefix(":").substringBefore("?").substringBefore("(")
PathSegment.Variable(varName)
} else {
PathSegment.Exact(part)
}
})

internal fun parseVaadinUrlMapping(urlMapping: String): UrlPath {
return vaadinUrlPksParser.parseUrlPath(PartiallyKnownString(urlMapping)).urlPath
}
15 changes: 15 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
<depends>org.jetbrains.idea.maven</depends>
<depends>org.jetbrains.plugins.gradle</depends>


<!-- Vaadin Endpoints IntelliJ Ultimate dependencies -->
<depends>com.intellij.properties</depends>
<depends optional="true">com.intellij.modules.microservices</depends>
Copy link
Member Author

Choose a reason for hiding this comment

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

Optional to make plugin work with community edition also

<depends optional="true">com.intellij.microservices.jvm</depends>
<depends optional="true">com.intellij.modules.ultimate</depends>

<change-notes>
<![CDATA[
<p>Check <a href="https://github.com/vaadin/intellij-plugin/releases">release notes at GitHub</a> for more information.</p>
Expand Down Expand Up @@ -71,6 +78,14 @@

<!-- Configuration check -->
<notificationGroup id="Vaadin configuration check" displayType="STICKY_BALLOON"/>

<!-- Vaadin Endpoints -->
<psi.referenceContributor language="UAST" implementation="com.vaadin.plugin.endpoints.VaadinReferenceContributor"/>
<microservices.endpointsProvider implementation="com.vaadin.plugin.endpoints.VaadinEndpointsProvider"/>
<microservices.urlResolverFactory implementation="com.vaadin.plugin.endpoints.VaadinUrlResolverFactory"/>
<implicitUsageProvider implementation="com.vaadin.plugin.endpoints.VaadinImplicitUsageProvider"/>

<dependencySupport kind="java" coordinate="com.vaadin:flow-server" displayName="Vaadin Flow"/>
</extensions>

<actions>
Expand Down