Skip to content

Commit 98a20d3

Browse files
authored
Merge pull request #7 from dmitrish/develop
show usage as code snippet on hover
2 parents 130ff11 + d5ac481 commit 98a20d3

3 files changed

Lines changed: 85 additions & 83 deletions

File tree

src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageIconGenerator.kt

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,37 @@ import javax.swing.ImageIcon
99

1010
object ResourceUsageIconGenerator {
1111

12-
private const val ICON_SIZE = 16
12+
private const val ICON_SIZE = 21 // 1/3 bigger (was 16)
1313

1414
fun createIcon(usageCount: Int): Icon {
15-
val color = when {
16-
usageCount == 0 -> Color(128, 128, 128) // Grey
17-
usageCount == 1 -> Color(66, 133, 244) // Blue
18-
else -> Color(234, 67, 53) // Red
15+
// Cap at 99+
16+
val displayText = if (usageCount > 99) "99+" else usageCount.toString()
17+
18+
val backgroundColor = when (usageCount) {
19+
0 -> Color(128, 128, 128) // Grey
20+
1 -> Color(66, 133, 244) // Blue
21+
else -> Color(234, 67, 53) // Red
1922
}
2023

21-
return createCircleIcon(usageCount.toString(), color)
24+
return createCircleIcon(displayText, backgroundColor)
2225
}
2326

2427
fun createLoadingIcon(): Icon {
25-
return createCircleIcon("?", Color(200, 200, 200))
28+
return createCircleIcon("?", Color(128, 128, 128))
2629
}
2730

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

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

3638
// Draw circle
3739
g2d.color = backgroundColor
3840
g2d.fillOval(0, 0, ICON_SIZE, ICON_SIZE)
3941

40-
// Draw text
42+
// Draw text with original font size
4143
g2d.color = Color.WHITE
4244
g2d.font = Font("SansSerif", Font.BOLD, 10)
4345
val metrics = g2d.fontMetrics

src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageLineMarkerProvider.kt

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,14 @@ package com.coroutines.androidresourceusagetracker
22

33
import com.intellij.codeInsight.daemon.LineMarkerInfo
44
import com.intellij.codeInsight.daemon.LineMarkerProvider
5-
import com.intellij.openapi.application.ApplicationManager
65
import com.intellij.openapi.editor.markup.GutterIconRenderer
7-
import com.intellij.openapi.project.Project
8-
import com.intellij.openapi.util.Key
96
import com.intellij.psi.PsiElement
107
import com.intellij.psi.xml.XmlTag
11-
import com.intellij.util.concurrency.AppExecutorUtil
12-
import java.util.concurrent.ConcurrentHashMap
13-
import java.util.concurrent.TimeUnit
148

159
class ResourceUsageLineMarkerProvider : LineMarkerProvider {
1610

17-
companion object {
18-
private val CACHE = ConcurrentHashMap<String, Int>()
19-
private val COMPUTING = ConcurrentHashMap<String, Boolean>()
20-
private const val CACHE_INVALIDATION_DELAY_MS = 5000L
21-
}
22-
2311
override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
24-
return null // All work happens in collectSlowLineMarkers
12+
return null
2513
}
2614

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

3523
val resourceName = element.getAttributeValue("name") ?: continue
36-
val project = element.project
37-
val cacheKey = getCacheKey(project, resourceName)
38-
39-
// Get cached count or use placeholder
40-
val usageCount = CACHE.getOrDefault(cacheKey, -1)
41-
42-
// Create icon (use placeholder for -1)
43-
val icon = if (usageCount >= 0) {
44-
ResourceUsageIconGenerator.createIcon(usageCount)
45-
} else {
46-
ResourceUsageIconGenerator.createLoadingIcon()
47-
}
24+
val usageCount = UsageCounter.countUsages(element)
4825

49-
// Create line marker
26+
val icon = ResourceUsageIconGenerator.createIcon(usageCount)
5027
val anchorElement = element.firstChild ?: continue
28+
5129
val lineMarker = LineMarkerInfo(
5230
anchorElement,
5331
element.textRange,
5432
icon,
55-
{ if (usageCount >= 0) "Used $usageCount time${if (usageCount != 1) "s" else ""}" else "Computing..." },
33+
{ createTooltip(resourceName, usageCount, element) },
5634
null,
5735
GutterIconRenderer.Alignment.RIGHT
5836
)
5937

6038
result.add(lineMarker)
61-
62-
// Start async computation if not cached
63-
if (usageCount == -1 && COMPUTING.putIfAbsent(cacheKey, true) == null) {
64-
scheduleUsageCountComputation(element, resourceName, cacheKey, project)
65-
}
6639
}
6740
}
6841

69-
private fun scheduleUsageCountComputation(
70-
element: XmlTag,
71-
resourceName: String,
72-
cacheKey: String,
73-
project: Project
74-
) {
75-
AppExecutorUtil.getAppScheduledExecutorService().schedule({
76-
try {
77-
val count = UsageCounter.countUsages(element)
78-
CACHE[cacheKey] = count
79-
80-
// Schedule cache invalidation
81-
AppExecutorUtil.getAppScheduledExecutorService().schedule({
82-
CACHE.remove(cacheKey)
83-
}, CACHE_INVALIDATION_DELAY_MS, TimeUnit.MILLISECONDS)
84-
85-
// Trigger UI update
86-
ApplicationManager.getApplication().invokeLater {
87-
com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
88-
.getInstance(project)
89-
.restart()
90-
}
91-
} finally {
92-
COMPUTING.remove(cacheKey)
42+
private fun createTooltip(resourceName: String, usageCount: Int, element: XmlTag): String {
43+
if (usageCount == 0) {
44+
return "$resourceName: Not used"
45+
}
46+
47+
val usages = UsageCounter.getUsages(element)
48+
val displayCount = if (usageCount > 99) "99+" else usageCount.toString()
49+
50+
val tooltip = buildString {
51+
append("<html>")
52+
append("<b>$resourceName</b>: $displayCount usage${if (usageCount != 1) "s" else ""}<br><br>")
53+
54+
// Show up to 5 usages in tooltip
55+
usages.take(5).forEach { usage ->
56+
append("<b>${usage.filePath}:${usage.lineNumber}</b><br>")
57+
append("<code>${usage.codeSnippet}</code><br><br>")
9358
}
94-
}, 100, TimeUnit.MILLISECONDS) // Small delay to batch requests
95-
}
9659

97-
private fun getCacheKey(project: Project, resourceName: String): String {
98-
return "${project.name}:$resourceName"
60+
if (usages.size > 5) {
61+
append("<i>...and ${usages.size - 5} more</i>")
62+
}
63+
64+
append("</html>")
65+
}
66+
67+
return tooltip
9968
}
10069

10170
private fun isAndroidResourceTag(tag: XmlTag): Boolean {
102-
val tagName = tag.name
10371
val validTags = setOf(
10472
"string", "color", "dimen", "style", "drawable",
10573
"integer", "bool", "array", "string-array", "integer-array",
10674
"plurals", "attr", "declare-styleable", "item", "id"
10775
)
108-
return validTags.contains(tagName) && tag.getAttribute("name") != null
76+
return validTags.contains(tag.name) && tag.getAttribute("name") != null
10977
}
110-
}
78+
}
79+

src/main/kotlin/com/coroutines/androidresourceusagetracker/UsageCounter.kt

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coroutines.androidresourceusagetracker
33
import com.intellij.openapi.application.ReadAction
44
import com.intellij.openapi.diagnostic.Logger
55
import com.intellij.openapi.module.ModuleUtilCore
6+
import com.intellij.openapi.progress.ProcessCanceledException
67
import com.intellij.psi.PsiElement
78
import com.intellij.psi.search.GlobalSearchScope
89
import com.intellij.psi.search.PsiSearchHelper
@@ -11,23 +12,33 @@ import com.intellij.psi.search.UsageSearchContext
1112
import com.intellij.psi.xml.XmlTag
1213
import com.intellij.psi.xml.XmlAttributeValue
1314

15+
data class ResourceUsage(
16+
val filePath: String,
17+
val lineNumber: Int,
18+
val codeSnippet: String
19+
)
20+
1421
object UsageCounter {
1522

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

1825
fun countUsages(element: XmlTag): Int {
19-
return ReadAction.compute<Int, RuntimeException> {
26+
return getUsages(element).size
27+
}
28+
29+
fun getUsages(element: XmlTag): List<ResourceUsage> {
30+
return ReadAction.compute<List<ResourceUsage>, RuntimeException> {
2031
try {
21-
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return@compute 0
22-
val resourceName = element.getAttributeValue("name") ?: return@compute 0
23-
val resourceType = getResourceType(element) ?: return@compute 0
32+
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return@compute emptyList()
33+
val resourceName = element.getAttributeValue("name") ?: return@compute emptyList()
34+
val resourceType = getResourceType(element) ?: return@compute emptyList()
2435
val project = element.project
2536
val definitionFile = element.containingFile
2637

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

30-
var usageCount = 0
41+
val usages = mutableListOf<ResourceUsage>()
3142
val seenLocations = mutableSetOf<String>()
3243

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

6274
if (!seenLocations.contains(key)) {
6375
seenLocations.add(key)
64-
usageCount++
65-
LOG.info(" ✓ Counted usage at $key")
76+
77+
// Extract code snippet
78+
val lineText = if (document != null && line >= 0) {
79+
val lineStart = document.getLineStartOffset(line)
80+
val lineEnd = document.getLineEndOffset(line)
81+
document.getText(com.intellij.openapi.util.TextRange(lineStart, lineEnd)).trim()
82+
} else {
83+
psiElement.text
84+
}
85+
86+
// Get relative file path
87+
val relativePath = path.substringAfterLast("/app/src/main/", path.substringAfterLast("/"))
88+
89+
usages.add(ResourceUsage(
90+
filePath = relativePath,
91+
lineNumber = line + 1, // 1-indexed for display
92+
codeSnippet = lineText
93+
))
6694
}
6795
}
6896
}
@@ -75,12 +103,15 @@ object UsageCounter {
75103
true
76104
)
77105

78-
LOG.info("Found $usageCount actual usage(s) of $resourceType/$resourceName")
79-
return@compute usageCount
106+
LOG.info("Found ${usages.size} actual usage(s) of $resourceType/$resourceName")
107+
return@compute usages
80108

109+
} catch (e: ProcessCanceledException) {
110+
// CRITICAL: Rethrow ProcessCanceledException - never catch it!
111+
throw e
81112
} catch (e: Exception) {
82113
LOG.error("Error counting usages", e)
83-
return@compute 0
114+
return@compute emptyList()
84115
}
85116
}
86117
}

0 commit comments

Comments
 (0)