From d5ac48169f449939213cb3816898bc3764a0dfe0 Mon Sep 17 00:00:00 2001 From: Dmitri Sh Date: Sun, 28 Dec 2025 17:39:50 -0500 Subject: [PATCH] show usage as code snippet on hover --- .../ResourceUsageIconGenerator.kt | 20 ++-- .../ResourceUsageLineMarkerProvider.kt | 95 +++++++------------ .../UsageCounter.kt | 53 ++++++++--- 3 files changed, 85 insertions(+), 83 deletions(-) diff --git a/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageIconGenerator.kt b/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageIconGenerator.kt index a0cb349..119c51c 100644 --- a/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageIconGenerator.kt +++ b/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageIconGenerator.kt @@ -9,27 +9,29 @@ 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) @@ -37,7 +39,7 @@ object ResourceUsageIconGenerator { 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 diff --git a/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageLineMarkerProvider.kt b/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageLineMarkerProvider.kt index bb8d257..8cf6101 100644 --- a/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageLineMarkerProvider.kt +++ b/src/main/kotlin/com/coroutines/androidresourceusagetracker/ResourceUsageLineMarkerProvider.kt @@ -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() - private val COMPUTING = ConcurrentHashMap() - 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( @@ -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("") + append("$resourceName: $displayCount usage${if (usageCount != 1) "s" else ""}

") + + // Show up to 5 usages in tooltip + usages.take(5).forEach { usage -> + append("${usage.filePath}:${usage.lineNumber}
") + append("${usage.codeSnippet}

") } - }, 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("...and ${usages.size - 5} more") + } + + append("") + } + + 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 } -} \ No newline at end of file +} + diff --git a/src/main/kotlin/com/coroutines/androidresourceusagetracker/UsageCounter.kt b/src/main/kotlin/com/coroutines/androidresourceusagetracker/UsageCounter.kt index 27a53a1..0a3dc2b 100644 --- a/src/main/kotlin/com/coroutines/androidresourceusagetracker/UsageCounter.kt +++ b/src/main/kotlin/com/coroutines/androidresourceusagetracker/UsageCounter.kt @@ -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 @@ -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 { + return getUsages(element).size + } + + fun getUsages(element: XmlTag): List { + return ReadAction.compute, 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() val seenLocations = mutableSetOf() searchHelper.processElementsWithWord( @@ -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 + )) } } } @@ -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() } } }