@@ -3,77 +3,261 @@ package com.coroutines.androidresourceusagetracker
33import com.intellij.codeInsight.daemon.LineMarkerInfo
44import com.intellij.codeInsight.daemon.LineMarkerProvider
55import com.intellij.openapi.editor.markup.GutterIconRenderer
6+ import com.intellij.openapi.fileEditor.OpenFileDescriptor
7+ import com.intellij.openapi.project.Project
8+ import com.intellij.openapi.vfs.LocalFileSystem
69import com.intellij.psi.PsiElement
710import com.intellij.psi.xml.XmlTag
11+ import com.intellij.ui.Gray
12+ import com.intellij.ui.JBColor
13+ import java.awt.*
14+ import java.awt.event.MouseAdapter
15+ import java.awt.event.MouseEvent
16+ import java.awt.geom.RoundRectangle2D
17+ import javax.swing.*
18+ import javax.swing.border.EmptyBorder
819
920class ResourceUsageLineMarkerProvider : LineMarkerProvider {
1021
22+ /*
1123 override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
12- return null
24+ if (element !is XmlTag) return null
25+ if (element.name !in listOf("string", "color", "dimen", "style", "drawable", "integer", "bool", "array", "string-array", "integer-array", "plurals", "id")) {
26+ return null
27+ }
28+
29+ val resourceName = element.getAttributeValue("name") ?: return null
30+ val count = UsageCounter.countUsages(element)
31+
32+ return LineMarkerInfo(
33+ element,
34+ element.textRange,
35+ createUsageIcon(count),
36+ { "$count usage${if (count != 1) "s" else ""}" },
37+ { e, elt ->
38+ if (count > 0) {
39+ showUsagesPopup(e, elt as XmlTag, elt.project)
40+ }
41+ },
42+ GutterIconRenderer.Alignment.RIGHT,
43+ { "$count usage${if (count != 1) "s" else ""}" }
44+ )
1345 }
1446
15- override fun collectSlowLineMarkers (
16- elements : List <PsiElement >,
17- result : MutableCollection <in LineMarkerInfo <* >>
18- ) {
19- for (element in elements) {
20- if (element !is XmlTag ) continue
21- if (! isAndroidResourceTag(element)) continue
22-
23- val resourceName = element.getAttributeValue(" name" ) ? : continue
24- val usageCount = UsageCounter .countUsages(element)
25-
26- val icon = ResourceUsageIconGenerator .createIcon(usageCount)
27- val anchorElement = element.firstChild ? : continue
28-
29- val lineMarker = LineMarkerInfo (
30- anchorElement,
31- element.textRange,
32- icon,
33- { createTooltip(resourceName, usageCount, element) },
34- null ,
35- GutterIconRenderer .Alignment .RIGHT
36- )
47+ */
48+
49+ override fun getLineMarkerInfo (element : PsiElement ): LineMarkerInfo <* >? {
50+ // Only process the tag name identifier (leaf element), not the whole tag
51+ if (element !is com.intellij.psi.xml.XmlToken ) return null
52+ if (element.tokenType != com.intellij.psi.xml.XmlTokenType .XML_NAME ) return null
3753
38- result.add(lineMarker)
54+ // Make sure this is the opening tag name, not closing tag or attribute name
55+ // The previous sibling should be XML_START_TAG_START ("<")
56+ val prevSibling = element.prevSibling
57+ if (prevSibling !is com.intellij.psi.xml.XmlToken ||
58+ prevSibling.tokenType != com.intellij.psi.xml.XmlTokenType .XML_START_TAG_START ) {
59+ return null
3960 }
61+
62+ val parent = element.parent
63+ if (parent !is XmlTag ) return null
64+
65+ // Check if this is a resource tag we care about
66+ if (parent.name !in listOf (" string" , " color" , " dimen" , " style" , " drawable" , " integer" , " bool" , " array" , " string-array" , " integer-array" , " plurals" , " id" )) {
67+ return null
68+ }
69+
70+ // Only process if this tag has a "name" attribute (it's a resource definition)
71+ val resourceName = parent.getAttributeValue(" name" ) ? : return null
72+
73+ // Make sure we're in a values XML file (check parent directory name)
74+ val parentDirName = element.containingFile.virtualFile?.parent?.name ? : " "
75+ if (! parentDirName.contains(" values" )) {
76+ return null
77+ }
78+
79+ val count = UsageCounter .countUsages(parent)
80+
81+ return LineMarkerInfo (
82+ element, // Register on the leaf element (XML_NAME token)
83+ element.textRange,
84+ createUsageIcon(count),
85+ { " $count usage${if (count != 1 ) " s" else " " } " },
86+ { e, elt ->
87+ if (count > 0 ) {
88+ // Navigate up to the XmlTag for processing
89+ val tag = elt.parent as ? XmlTag ? : return @LineMarkerInfo
90+ showUsagesPopup(e, tag, tag.project)
91+ }
92+ },
93+ GutterIconRenderer .Alignment .RIGHT ,
94+ { " $count usage${if (count != 1 ) " s" else " " } " }
95+ )
4096 }
97+ private fun createUsageIcon (count : Int ): Icon {
98+ return object : Icon {
99+ override fun getIconWidth () = 21
100+ override fun getIconHeight () = 21
101+
102+ override fun paintIcon (c : Component ? , g : Graphics ? , x : Int , y : Int ) {
103+ val g2d = g as Graphics2D
104+ g2d.setRenderingHint(RenderingHints .KEY_ANTIALIASING , RenderingHints .VALUE_ANTIALIAS_ON )
105+
106+ val color = when {
107+ count == 0 -> JBColor (Color (200 , 200 , 200 ), Gray ._100 )
108+ count < 5 -> JBColor (Color (100 , 180 , 255 ), Color (80 , 140 , 200 ))
109+ else -> JBColor (Color (100 , 200 , 100 ), Color (80 , 160 , 80 ))
110+ }
111+
112+ g2d.color = color
113+ g2d.fill(RoundRectangle2D .Double (x.toDouble(), y.toDouble(), 20.0 , 20.0 , 6.0 , 6.0 ))
41114
42- private fun createTooltip (resourceName : String , usageCount : Int , element : XmlTag ): String {
43- if (usageCount == 0 ) {
44- return " $resourceName : Not used"
115+ g2d.color = JBColor .WHITE
116+ g2d.font = Font (" SansSerif" , Font .BOLD , 11 )
117+ val text = if (count > 99 ) " 99+" else count.toString()
118+ val fm = g2d.fontMetrics
119+ val textWidth = fm.stringWidth(text)
120+ val textX = x + (20 - textWidth) / 2
121+ val textY = y + ((20 - fm.height) / 2 ) + fm.ascent
122+ g2d.drawString(text, textX, textY)
123+ }
45124 }
125+ }
46126
127+ private fun showUsagesPopup (event : MouseEvent , element : XmlTag , project : Project ) {
47128 val usages = UsageCounter .getUsages(element)
48- val displayCount = if (usageCount > 99 ) " 99+ " else usageCount.toString()
129+ if (usages.isEmpty()) return
49130
50- val tooltip = buildString {
51- append(" <html>" )
52- append(" <b>$resourceName </b>: $displayCount usage${if (usageCount != 1 ) " s" else " " } <br><br>" )
131+ val popup = JWindow ()
132+ popup.type = Window .Type .POPUP
53133
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>" )
58- }
134+ val panel = JPanel (BorderLayout ()).apply {
135+ border = BorderFactory .createCompoundBorder(
136+ BorderFactory .createLineBorder(JBColor .border(), 1 ),
137+ EmptyBorder (8 , 8 , 8 , 8 )
138+ )
139+ background = JBColor .background()
140+ }
59141
60- if (usages.size > 5 ) {
61- append(" <i>...and ${usages.size - 5 } more</i>" )
62- }
142+ val titleLabel = JLabel (" ${usages.size} usage${if (usages.size != 1 ) " s" else " " } " ).apply {
143+ font = font.deriveFont(Font .BOLD , 13f )
144+ border = EmptyBorder (0 , 0 , 8 , 0 )
145+ }
63146
64- append(" </html>" )
147+ panel.add(titleLabel, BorderLayout .NORTH )
148+
149+ val usagesList = createUsagesList(usages, project)
150+ val scrollPane = JScrollPane (usagesList).apply {
151+ preferredSize = Dimension (600 , minOf(300 , usages.size * 60 + 20 ))
152+ border = null
65153 }
66154
67- return tooltip
155+ panel.add(scrollPane, BorderLayout .CENTER )
156+
157+ popup.contentPane = panel
158+ popup.pack()
159+
160+ val locationOnScreen = event.component.locationOnScreen
161+ popup.setLocation(locationOnScreen.x + event.x + 10 , locationOnScreen.y + event.y)
162+
163+ popup.isVisible = true
164+ // popup.isFocusableWindowState = true
165+ popup.requestFocus()
166+
167+ popup.addWindowFocusListener(object : java.awt.event.WindowFocusListener {
168+ override fun windowGainedFocus (e : java.awt.event.WindowEvent ? ) {}
169+ override fun windowLostFocus (e : java.awt.event.WindowEvent ? ) {
170+ popup.dispose()
171+ }
172+ })
68173 }
69174
70- private fun isAndroidResourceTag (tag : XmlTag ): Boolean {
71- val validTags = setOf (
72- " string" , " color" , " dimen" , " style" , " drawable" ,
73- " integer" , " bool" , " array" , " string-array" , " integer-array" ,
74- " plurals" , " attr" , " declare-styleable" , " item" , " id"
75- )
76- return validTags.contains(tag.name) && tag.getAttribute(" name" ) != null
175+ private fun createUsagesList (usages : List <ResourceUsage >, project : Project ): JList <ResourceUsage > {
176+ val listModel = DefaultListModel <ResourceUsage >()
177+ usages.forEach { listModel.addElement(it) }
178+
179+ return JList (listModel).apply {
180+ cellRenderer = UsageCellRenderer ()
181+ selectionMode = ListSelectionModel .SINGLE_SELECTION
182+
183+ addMouseListener(object : MouseAdapter () {
184+ override fun mouseClicked (e : MouseEvent ) {
185+ if (e.clickCount == 2 ) {
186+ val usage = selectedValue ? : return
187+
188+ // Use the full absolute path directly for navigation
189+ val virtualFile = LocalFileSystem .getInstance().findFileByPath(usage.filePath)
190+
191+ if (virtualFile != null ) {
192+ val descriptor = OpenFileDescriptor (project, virtualFile, usage.lineNumber - 1 , 0 )
193+ descriptor.navigate(true )
194+ }
195+ }
196+ }
197+ })
198+ }
77199 }
78- }
79200
201+ private class UsageCellRenderer : DefaultListCellRenderer () {
202+ override fun getListCellRendererComponent (
203+ list : JList <* >? ,
204+ value : Any? ,
205+ index : Int ,
206+ isSelected : Boolean ,
207+ cellHasFocus : Boolean
208+ ): Component {
209+ val usage = value as ResourceUsage
210+
211+ val panel = JPanel (BorderLayout ()).apply {
212+ border = EmptyBorder (4 , 8 , 4 , 8 )
213+ background = if (isSelected) JBColor .background() else JBColor .background()
214+ }
215+
216+ // Get display-friendly path
217+ val displayPath = getDisplayPath(usage.filePath)
218+
219+ val fileLabel = JLabel (" <html><b style='color: #589df6;'>$displayPath :${usage.lineNumber} </b></html>" ).apply {
220+ font = Font (" Monospaced" , Font .PLAIN , 12 )
221+ }
222+
223+ val codeLabel = JLabel (" <html><span style='color: #808080; font-family: monospace;'>${escapeHtml(usage.codeSnippet)} </span></html>" ).apply {
224+ font = Font (" Monospaced" , Font .PLAIN , 11 )
225+ }
226+
227+ val labelsPanel = JPanel ().apply {
228+ layout = BoxLayout (this , BoxLayout .Y_AXIS )
229+ add(fileLabel)
230+ add(Box .createVerticalStrut(2 ))
231+ add(codeLabel)
232+ background = if (isSelected) JBColor .background() else JBColor .background()
233+ }
234+
235+ panel.add(labelsPanel, BorderLayout .CENTER )
236+
237+ if (isSelected) {
238+ panel.background = JBColor (Color (220 , 230 , 240 ), Color (60 , 70 , 80 ))
239+ labelsPanel.background = panel.background
240+ }
241+
242+ return panel
243+ }
244+
245+ private fun getDisplayPath (path : String ): String {
246+ // Try to get path relative to common source roots for display
247+ return when {
248+ path.contains(" /src/main/" ) -> path.substringAfter(" /src/main/" )
249+ path.contains(" /src/test/" ) -> path.substringAfter(" /src/test/" )
250+ path.contains(" /src/androidTest/" ) -> path.substringAfter(" /src/androidTest/" )
251+ else -> path.substringAfterLast(" /" )
252+ }
253+ }
254+
255+ private fun escapeHtml (text : String ): String {
256+ return text
257+ .replace(" &" , " &" )
258+ .replace(" <" , " <" )
259+ .replace(" >" , " >" )
260+ .replace(" \" " , " "" )
261+ }
262+ }
263+ }
0 commit comments