Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,37 @@ import javax.swing.ImageIcon

object ResourceUsageIconGenerator {

private const val ICON_SIZE = 16
private const val ICON_SIZE = 21 // 1/3 bigger (was 16)

fun createIcon(usageCount: Int): Icon {
val color = when {
usageCount == 0 -> Color(128, 128, 128) // Grey
usageCount == 1 -> Color(66, 133, 244) // Blue
else -> Color(234, 67, 53) // Red
// Cap at 99+
val displayText = if (usageCount > 99) "99+" else usageCount.toString()

val backgroundColor = when (usageCount) {
0 -> Color(128, 128, 128) // Grey
1 -> Color(66, 133, 244) // Blue
else -> Color(234, 67, 53) // Red
}

return createCircleIcon(usageCount.toString(), color)
return createCircleIcon(displayText, backgroundColor)
}

fun createLoadingIcon(): Icon {
return createCircleIcon("?", Color(200, 200, 200))
return createCircleIcon("?", Color(128, 128, 128))
}

private fun createCircleIcon(text: String, backgroundColor: Color): Icon {
val image = BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_INT_ARGB)
val g2d = image.createGraphics()

// Enable anti-aliasing
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)

// Draw circle
g2d.color = backgroundColor
g2d.fillOval(0, 0, ICON_SIZE, ICON_SIZE)

// Draw text
// Draw text with original font size
g2d.color = Color.WHITE
g2d.font = Font("SansSerif", Font.BOLD, 10)
val metrics = g2d.fontMetrics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,14 @@ package com.coroutines.androidresourceusagetracker

import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProvider
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.markup.GutterIconRenderer
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.xml.XmlTag
import com.intellij.util.concurrency.AppExecutorUtil
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit

class ResourceUsageLineMarkerProvider : LineMarkerProvider {

companion object {
private val CACHE = ConcurrentHashMap<String, Int>()
private val COMPUTING = ConcurrentHashMap<String, Boolean>()
private const val CACHE_INVALIDATION_DELAY_MS = 5000L
}

override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
return null // All work happens in collectSlowLineMarkers
return null
}

override fun collectSlowLineMarkers(
Expand All @@ -33,78 +21,59 @@ class ResourceUsageLineMarkerProvider : LineMarkerProvider {
if (!isAndroidResourceTag(element)) continue

val resourceName = element.getAttributeValue("name") ?: continue
val project = element.project
val cacheKey = getCacheKey(project, resourceName)

// Get cached count or use placeholder
val usageCount = CACHE.getOrDefault(cacheKey, -1)

// Create icon (use placeholder for -1)
val icon = if (usageCount >= 0) {
ResourceUsageIconGenerator.createIcon(usageCount)
} else {
ResourceUsageIconGenerator.createLoadingIcon()
}
val usageCount = UsageCounter.countUsages(element)

// Create line marker
val icon = ResourceUsageIconGenerator.createIcon(usageCount)
val anchorElement = element.firstChild ?: continue

val lineMarker = LineMarkerInfo(
anchorElement,
element.textRange,
icon,
{ if (usageCount >= 0) "Used $usageCount time${if (usageCount != 1) "s" else ""}" else "Computing..." },
{ createTooltip(resourceName, usageCount, element) },
null,
GutterIconRenderer.Alignment.RIGHT
)

result.add(lineMarker)

// Start async computation if not cached
if (usageCount == -1 && COMPUTING.putIfAbsent(cacheKey, true) == null) {
scheduleUsageCountComputation(element, resourceName, cacheKey, project)
}
}
}

private fun scheduleUsageCountComputation(
element: XmlTag,
resourceName: String,
cacheKey: String,
project: Project
) {
AppExecutorUtil.getAppScheduledExecutorService().schedule({
try {
val count = UsageCounter.countUsages(element)
CACHE[cacheKey] = count

// Schedule cache invalidation
AppExecutorUtil.getAppScheduledExecutorService().schedule({
CACHE.remove(cacheKey)
}, CACHE_INVALIDATION_DELAY_MS, TimeUnit.MILLISECONDS)

// Trigger UI update
ApplicationManager.getApplication().invokeLater {
com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
.getInstance(project)
.restart()
}
} finally {
COMPUTING.remove(cacheKey)
private fun createTooltip(resourceName: String, usageCount: Int, element: XmlTag): String {
if (usageCount == 0) {
return "$resourceName: Not used"
}

val usages = UsageCounter.getUsages(element)
val displayCount = if (usageCount > 99) "99+" else usageCount.toString()

val tooltip = buildString {
append("<html>")
append("<b>$resourceName</b>: $displayCount usage${if (usageCount != 1) "s" else ""}<br><br>")

// Show up to 5 usages in tooltip
usages.take(5).forEach { usage ->
append("<b>${usage.filePath}:${usage.lineNumber}</b><br>")
append("<code>${usage.codeSnippet}</code><br><br>")
}
}, 100, TimeUnit.MILLISECONDS) // Small delay to batch requests
}

private fun getCacheKey(project: Project, resourceName: String): String {
return "${project.name}:$resourceName"
if (usages.size > 5) {
append("<i>...and ${usages.size - 5} more</i>")
}

append("</html>")
}

return tooltip
}

private fun isAndroidResourceTag(tag: XmlTag): Boolean {
val tagName = tag.name
val validTags = setOf(
"string", "color", "dimen", "style", "drawable",
"integer", "bool", "array", "string-array", "integer-array",
"plurals", "attr", "declare-styleable", "item", "id"
)
return validTags.contains(tagName) && tag.getAttribute("name") != null
return validTags.contains(tag.name) && tag.getAttribute("name") != null
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.coroutines.androidresourceusagetracker
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.PsiSearchHelper
Expand All @@ -11,23 +12,33 @@ import com.intellij.psi.search.UsageSearchContext
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlAttributeValue

data class ResourceUsage(
val filePath: String,
val lineNumber: Int,
val codeSnippet: String
)

object UsageCounter {

private val LOG = Logger.getInstance(UsageCounter::class.java)

fun countUsages(element: XmlTag): Int {
return ReadAction.compute<Int, RuntimeException> {
return getUsages(element).size
}

fun getUsages(element: XmlTag): List<ResourceUsage> {
return ReadAction.compute<List<ResourceUsage>, RuntimeException> {
try {
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return@compute 0
val resourceName = element.getAttributeValue("name") ?: return@compute 0
val resourceType = getResourceType(element) ?: return@compute 0
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return@compute emptyList()
val resourceName = element.getAttributeValue("name") ?: return@compute emptyList()
val resourceType = getResourceType(element) ?: return@compute emptyList()
val project = element.project
val definitionFile = element.containingFile

val scope = GlobalSearchScope.moduleScope(module)
val searchHelper = PsiSearchHelper.getInstance(project)

var usageCount = 0
val usages = mutableListOf<ResourceUsage>()
val seenLocations = mutableSetOf<String>()

searchHelper.processElementsWithWord(
Expand Down Expand Up @@ -56,13 +67,30 @@ object UsageCounter {
// Only count specific PSI element types that represent actual references
if (isLeafReference(psiElement, resourceType, resourceName)) {
// Create unique key: file + line number
val line = containingFile.viewProvider.document?.getLineNumber(psiElement.textRange.startOffset) ?: -1
val document = containingFile.viewProvider.document
val line = document?.getLineNumber(psiElement.textRange.startOffset) ?: -1
val key = "$path:$line"

if (!seenLocations.contains(key)) {
seenLocations.add(key)
usageCount++
LOG.info(" ✓ Counted usage at $key")

// Extract code snippet
val lineText = if (document != null && line >= 0) {
val lineStart = document.getLineStartOffset(line)
val lineEnd = document.getLineEndOffset(line)
document.getText(com.intellij.openapi.util.TextRange(lineStart, lineEnd)).trim()
} else {
psiElement.text
}

// Get relative file path
val relativePath = path.substringAfterLast("/app/src/main/", path.substringAfterLast("/"))

usages.add(ResourceUsage(
filePath = relativePath,
lineNumber = line + 1, // 1-indexed for display
codeSnippet = lineText
))
}
}
}
Expand All @@ -75,12 +103,15 @@ object UsageCounter {
true
)

LOG.info("Found $usageCount actual usage(s) of $resourceType/$resourceName")
return@compute usageCount
LOG.info("Found ${usages.size} actual usage(s) of $resourceType/$resourceName")
return@compute usages

} catch (e: ProcessCanceledException) {
// CRITICAL: Rethrow ProcessCanceledException - never catch it!
throw e
} catch (e: Exception) {
LOG.error("Error counting usages", e)
return@compute 0
return@compute emptyList()
}
}
}
Expand Down
Loading