Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -6,15 +6,9 @@ import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser
import io.github.composegears.valkyrie.sdk.core.extensions.safeAs
import io.github.composegears.valkyrie.sdk.ir.util.aspectRatio
import io.github.composegears.valkyrie.sdk.ir.util.dominantShadeColor
import io.github.composegears.valkyrie.sdk.ir.xml.toVectorXmlString
import io.github.composegears.valkyrie.util.getOrCreateCachedIcon
import javax.swing.Icon
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtProperty

class ImageVectorCompletionContributor : CompletionContributor() {
Expand All @@ -32,7 +26,7 @@ class ImageVectorCompletionContributor : CompletionContributor() {
?.safeAs<KtProperty>()
?.let { element ->
if (!element.isVar) {
getOrCreateCachedIcon(element.containingKtFile)
element.containingKtFile.getOrCreateCachedIcon()
} else {
null
}
Expand All @@ -50,26 +44,6 @@ class ImageVectorCompletionContributor : CompletionContributor() {
}
}

private fun getOrCreateCachedIcon(ktFile: KtFile): Icon? {
val cachedValuesManager = CachedValuesManager.getManager(ktFile.project)

return cachedValuesManager.getCachedValue(ktFile) {
val icon = createIconFromKtFile(ktFile)
CachedValueProvider.Result.create(icon, ktFile)
}
}

private fun createIconFromKtFile(ktFile: KtFile): Icon? {
val irImageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile)
val vectorXml = irImageVector?.toVectorXmlString() ?: return null

return ImageVectorIcon(
vectorXml = vectorXml,
aspectRatio = irImageVector.aspectRatio,
dominantShade = irImageVector.dominantShadeColor,
)
}

private class ComposeColorLookupElementDecorator(
delegate: LookupElement,
private val icon: Icon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer
import com.intellij.pom.Navigatable
import com.intellij.psi.PsiElement
import com.intellij.psi.createSmartPointer
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import io.github.composegears.valkyrie.completion.ImageVectorIcon
import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser
import io.github.composegears.valkyrie.sdk.core.extensions.safeAs
import io.github.composegears.valkyrie.sdk.ir.core.IrImageVector
import io.github.composegears.valkyrie.sdk.ir.util.aspectRatio
import io.github.composegears.valkyrie.sdk.ir.util.dominantShadeColor
import io.github.composegears.valkyrie.sdk.ir.xml.toVectorXmlString
import io.github.composegears.valkyrie.util.getOrCreateGutterIcon
import io.github.composegears.valkyrie.util.isImageVector
import javax.swing.Icon
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
Expand All @@ -32,8 +26,7 @@ class ImageVectorGutterProvider : LineMarkerProvider {
elements.filterIsInstance<KtProperty>()
.filter { it.isImageVector() }
.forEach { property ->
val icon = getOrCreateGutterIcon(property)

val icon = property.getOrCreateGutterIcon()
icon?.let {
property.nameIdentifier?.let { identifier ->
// nameIdentifier is already a leaf (LeafPsiElement)
Expand All @@ -55,7 +48,7 @@ class ImageVectorGutterProvider : LineMarkerProvider {
.firstOrNull { it.isImageVector() }
?: return@mapNotNull null

val icon = getOrCreateGutterIcon(referencedProperty) ?: return@mapNotNull null
val icon = referencedProperty.getOrCreateGutterIcon() ?: return@mapNotNull null

// Get the leaf element (identifier) from the reference expression
// due to LineMarker is supposed to be registered for leaf elements only
Expand All @@ -71,46 +64,6 @@ class ImageVectorGutterProvider : LineMarkerProvider {
.forEach(result::add)
}

private fun getOrCreateGutterIcon(ktProperty: KtProperty): Icon? {
val cachedValuesManager = CachedValuesManager.getManager(ktProperty.project)

return cachedValuesManager.getCachedValue(ktProperty) {
val icon = ktProperty.createIcon()

CachedValueProvider.Result.create(icon, ktProperty)
}
}

private fun KtProperty.createIcon(): Icon? {
val irImageVector = parseImageVectorProperty(this) ?: return null
val vectorXml = irImageVector.toVectorXmlString()

return ImageVectorIcon(
vectorXml = vectorXml,
aspectRatio = irImageVector.aspectRatio,
dominantShade = irImageVector.dominantShadeColor,
)
}

private fun parseImageVectorProperty(property: KtProperty): IrImageVector? {
// Try parsing the current file
val containingFile = property.containingKtFile
ImageVectorPsiParser.parseToIrImageVector(containingFile)?.let { return it }

// For properties from libraries, navigate to decompiled/attached source
val navigationElement = property.navigationElement
if (navigationElement is KtProperty && navigationElement != property) {
val sourceFile = navigationElement.containingKtFile

// Only parse if we have actual source code (not a stub)
if (COMPILED_CODE_MARKER !in sourceFile.text) {
return ImageVectorPsiParser.parseToIrImageVector(sourceFile)
}
}

return null
}

private fun <T : PsiElement> createGutterIcon(
element: T,
icon: Icon,
Expand All @@ -132,15 +85,4 @@ class ImageVectorGutterProvider : LineMarkerProvider {
GutterIconRenderer.Alignment.LEFT,
{ "ImageVector Icon: $name" },
)

private companion object {
private const val COMPILED_CODE_MARKER = "/* compiled code */"

private val IMAGE_VECTOR_TYPES = setOf(
"ImageVector",
"androidx.compose.ui.graphics.vector.ImageVector",
)

private fun KtProperty.isImageVector(): Boolean = typeReference?.text in IMAGE_VECTOR_TYPES
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.composegears.valkyrie.icon

import com.intellij.ide.IconProvider
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import io.github.composegears.valkyrie.util.getOrCreateCachedIcon
import io.github.composegears.valkyrie.util.hasImageVectorProperties
import javax.swing.Icon
import org.jetbrains.kotlin.psi.KtFile

class ImageVectorIconProvider :
IconProvider(),
DumbAware {

override fun getIcon(element: PsiElement, flags: Int): Icon? {
val ktFile = when (element) {
is KtFile -> element
is PsiFile -> null
else -> element.containingFile as? KtFile
} ?: return null

if (!ktFile.hasImageVectorProperties()) {
return null
}

return ktFile.getOrCreateCachedIcon()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.github.composegears.valkyrie.util

import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import io.github.composegears.valkyrie.completion.ImageVectorIcon
import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser
import io.github.composegears.valkyrie.sdk.ir.core.IrImageVector
import io.github.composegears.valkyrie.sdk.ir.util.aspectRatio
import io.github.composegears.valkyrie.sdk.ir.util.dominantShadeColor
import io.github.composegears.valkyrie.sdk.ir.xml.toVectorXmlString
import javax.swing.Icon
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtProperty

/**
* Checks if this property is an ImageVector based on its type reference.
*/
fun KtProperty.isImageVector(): Boolean {
val typeText = typeReference?.text ?: return false
return typeText in IMAGE_VECTOR_TYPES
}

/**
* Checks if this Kotlin file contains ImageVector properties.
*
* A file is considered to contain ImageVector if it has at least one top-level
* property with an ImageVector type annotation.
*/
fun KtFile.hasImageVectorProperties(): Boolean {
val properties = declarations.filterIsInstance<KtProperty>()
if (properties.isEmpty()) return false

return properties.any { it.isImageVector() }
}

/**
* Gets or creates a cached gutter icon for an ImageVector property.
*
* This function uses the PSI caching mechanism to avoid recreating icons repeatedly
* for the same property, improving performance.
*/
fun KtFile.getOrCreateCachedIcon(): Icon? {
return CachedValuesManager
.getManager(project).getCachedValue(this) {
val icon = createImageVectorIcon()
CachedValueProvider.Result.create(icon, this)
}
}

/**
* Gets or creates a cached gutter icon for an ImageVector property.
*
* This function uses the PSI caching mechanism to avoid recreating icons repeatedly
* for the same property, improving performance.
*/
fun KtProperty.getOrCreateGutterIcon(): Icon? {
val cachedValuesManager = CachedValuesManager.getManager(project)

return cachedValuesManager.getCachedValue(this) {
val icon = createIcon()

CachedValueProvider.Result.create(icon, this)
}
}

/**
* Creates an icon representation from this Kotlin file containing an ImageVector.
*
* This function parses the ImageVector definition and renders it as a Swing Icon
* suitable for display in the IDE UI (project view, gutter, completion, etc.).
*/
private fun KtFile.createImageVectorIcon(): Icon? = ImageVectorPsiParser.parseToIrImageVector(this)
?.toIcon()

private fun KtProperty.createIcon(): Icon? = parseImageVectorProperty(this)
?.toIcon()

private fun IrImageVector.toIcon(): Icon = ImageVectorIcon(
vectorXml = toVectorXmlString(),
aspectRatio = aspectRatio,
dominantShade = dominantShadeColor,
)

private fun parseImageVectorProperty(property: KtProperty): IrImageVector? {
// Try parsing the current file
val containingFile = property.containingKtFile
ImageVectorPsiParser.parseToIrImageVector(containingFile)?.let { return it }

// For properties from libraries, navigate to decompiled/attached source
val navigationElement = property.navigationElement
if (navigationElement is KtProperty && navigationElement != property) {
val sourceFile = navigationElement.containingKtFile

// Only parse if we have actual source code (not a stub)
if (COMPILED_CODE_MARKER !in sourceFile.text) {
return ImageVectorPsiParser.parseToIrImageVector(sourceFile)
}
}

return null
}

private val IMAGE_VECTOR_TYPES = setOf(
"ImageVector",
"androidx.compose.ui.graphics.vector.ImageVector",
)

private const val COMPILED_CODE_MARKER = "/* compiled code */"
2 changes: 2 additions & 0 deletions tools/idea-plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Allows to create organized icon pack with an extension property of you pack obje
language="kotlin"
order="first, before KotlinCompletionContributor, before AndroidComposeCompletionContributor"
implementationClass="io.github.composegears.valkyrie.completion.ImageVectorCompletionContributor"/>

<iconProvider implementation="io.github.composegears.valkyrie.icon.ImageVectorIconProvider" order="first"/>
</extensions>

<extensions defaultExtensionNs="org.jetbrains.kotlin">
Expand Down