@@ -3,11 +3,16 @@ package com.github.xepozz.ide.introspector.core
33import com.github.xepozz.ide.introspector.core.internal.TtlCache
44import com.github.xepozz.ide.introspector.model.ExtensionInfo
55import com.github.xepozz.ide.introspector.model.ExtensionPointInfo
6+ import com.github.xepozz.ide.introspector.model.ListenerInfo
67import com.github.xepozz.ide.introspector.model.PluginDependencyInfo
78import com.github.xepozz.ide.introspector.model.PluginInfo
9+ import com.github.xepozz.ide.introspector.model.ServiceInfo
10+ import com.github.xepozz.ide.introspector.model.TopicInfo
811import com.intellij.ide.plugins.PluginManagerCore
912import com.intellij.openapi.components.Service
1013import com.intellij.openapi.components.service
14+ import com.intellij.openapi.extensions.PluginId
15+ import java.util.concurrent.ConcurrentHashMap
1116
1217/* *
1318 * Application-level cache of plugin + EP + extension data. Both the MCP arch.* tools
@@ -24,19 +29,67 @@ class PluginInventory {
2429 val plugins : List <PluginInfo >,
2530 val extensionPoints : List <ExtensionPointInfo >,
2631 val extensionsByEp : Map <String , List <ExtensionInfo >>,
32+ val servicesByPluginId : Map <String , List <ServiceInfo >>,
33+ val listenersByPluginId : Map <String , List <ListenerInfo >>,
2734 )
2835
2936 private val cache = TtlCache <Snapshot >(ttlMs = CACHE_TTL_MS ) { collect() }
37+ // Light-service walk is non-deterministic (depends on what's been touched in the IDE).
38+ // Kept on a shorter TTL and separate from the deterministic snapshot.
39+ private val lightServiceCache = TtlCache <List <ServiceInfo >>(ttlMs = LIGHT_SERVICE_TTL_MS ) {
40+ val xmlImpls = snapshot().servicesByPluginId.values.asSequence()
41+ .flatten().map { it.implementationClass }.toSet()
42+ ServiceInspector .listLightInstantiated(xmlImpls)
43+ }
44+ // Topics scanning walks every plugin's classpath and is expensive — kept OUT of the
45+ // main snapshot. Computed on demand per plugin and cached forever (until refresh()),
46+ // since a plugin's declared topics don't change while the IDE is running.
47+ private val topicsByPluginCache = ConcurrentHashMap <String , List <TopicInfo >>()
3048
3149 fun snapshot (forceRefresh : Boolean = false): Snapshot = cache.get(forceRefresh)
3250
33- fun refresh () { cache.invalidate(); cache.get() }
51+ fun refresh () {
52+ cache.invalidate()
53+ lightServiceCache.invalidate()
54+ topicsByPluginCache.clear()
55+ cache.get()
56+ }
3457
3558 fun plugins (): List <PluginInfo > = snapshot().plugins
3659 fun extensionPoints (): List <ExtensionPointInfo > = snapshot().extensionPoints
3760 fun extensionsByEp (): Map <String , List <ExtensionInfo >> = snapshot().extensionsByEp
3861 fun extensionsForEp (name : String ): List <ExtensionInfo > = extensionsByEp()[name] ? : emptyList()
3962
63+ fun services (): List <ServiceInfo > = snapshot().servicesByPluginId.values.flatten()
64+ fun listeners (): List <ListenerInfo > = snapshot().listenersByPluginId.values.flatten()
65+ fun servicesByPlugin (pluginId : String ): List <ServiceInfo > =
66+ snapshot().servicesByPluginId[pluginId] ? : emptyList()
67+ fun listenersByPlugin (pluginId : String ): List <ListenerInfo > =
68+ snapshot().listenersByPluginId[pluginId] ? : emptyList()
69+ /* *
70+ * Returns topics declared by [pluginId], scanning the plugin's classpath on first
71+ * request and caching the result. Returns empty if the plugin id is unknown or its
72+ * descriptor exposes no classloader.
73+ */
74+ fun topicsByPlugin (pluginId : String ): List <TopicInfo > = topicsByPluginCache.computeIfAbsent(pluginId) {
75+ val descriptor = PluginManagerCore .getPlugin(PluginId .getId(pluginId)) ? : return @computeIfAbsent emptyList()
76+ TopicInspector .listForPlugin(descriptor)
77+ }
78+
79+ /* *
80+ * Flattens topics across *already-scanned* plugins. Does NOT trigger scans for
81+ * unseen plugins — call [topicsByPlugin] (or [scanTopicsForPlugins]) first.
82+ */
83+ fun topics (): List <TopicInfo > = topicsByPluginCache.values.flatten()
84+
85+ /* * Forces topic scanning for the given plugin ids (each cached on first call). */
86+ fun scanTopicsForPlugins (pluginIds : Collection <String >) {
87+ for (id in pluginIds) topicsByPlugin(id)
88+ }
89+
90+ /* * Light services already created in this IDE session. Non-deterministic, separate TTL. */
91+ fun lightInstantiatedServices (): List <ServiceInfo > = lightServiceCache.get()
92+
4093 private fun collect (): Snapshot {
4194 val now = System .currentTimeMillis()
4295 val eps = ExtensionPointInspector .listExtensionPoints(" both" )
@@ -51,6 +104,15 @@ class PluginInventory {
51104 // Count EPs and extensions per plugin for the PluginInfo summary fields.
52105 val declaredCountByPluginId = eps.groupingBy { it.declaredByPluginId }.eachCount()
53106
107+ val servicesByPluginId = mutableMapOf<String , MutableList <ServiceInfo >>()
108+ for (s in ServiceInspector .listAll()) {
109+ servicesByPluginId.getOrPut(s.providedByPluginId) { mutableListOf () }.add(s)
110+ }
111+ val listenersByPluginId = mutableMapOf<String , MutableList <ListenerInfo >>()
112+ for (l in ListenerInspector .listAll()) {
113+ listenersByPluginId.getOrPut(l.providedByPluginId) { mutableListOf () }.add(l)
114+ }
115+
54116 @Suppress(" UnstableApiUsage" )
55117 val plugins = PluginManagerCore .plugins.map { descriptor ->
56118 val depList = descriptor.dependencies
@@ -60,22 +122,33 @@ class PluginInventory {
60122 optional = dep.isOptional,
61123 )
62124 }
125+ val id = descriptor.pluginId.idString
63126 PluginInfo (
64- id = descriptor.pluginId.idString ,
65- name = descriptor.name ? : descriptor.pluginId.idString ,
127+ id = id ,
128+ name = descriptor.name ? : id ,
66129 version = descriptor.version,
67130 vendor = descriptor.vendor,
68131 isBundled = descriptor.isBundled,
69132 isEnabled = readIsEnabled(descriptor),
70133 sinceBuild = descriptor.sinceBuild,
71134 untilBuild = descriptor.untilBuild,
72135 dependencies = depList,
73- declaredExtensionPointsCount = declaredCountByPluginId[descriptor.pluginId.idString ] ? : 0 ,
136+ declaredExtensionPointsCount = declaredCountByPluginId[id ] ? : 0 ,
74137 registeredExtensionsCount = 0 , // computed lazily when needed
138+ servicesCount = servicesByPluginId[id]?.size ? : 0 ,
139+ listenersCount = listenersByPluginId[id]?.size ? : 0 ,
140+ topicsCount = topicsByPluginCache[id]?.size ? : 0 ,
75141 )
76142 }.sortedBy { it.name.lowercase() }
77143
78- return Snapshot (now, plugins, eps, extensionsByEp)
144+ return Snapshot (
145+ takenAtMs = now,
146+ plugins = plugins,
147+ extensionPoints = eps,
148+ extensionsByEp = extensionsByEp,
149+ servicesByPluginId = servicesByPluginId.mapValues { it.value.toList() },
150+ listenersByPluginId = listenersByPluginId.mapValues { it.value.toList() },
151+ )
79152 }
80153
81154 /* * Lazily computes the extensions for a single EP and updates the cache map. */
@@ -103,6 +176,7 @@ class PluginInventory {
103176
104177 companion object {
105178 const val CACHE_TTL_MS = 30_000L
179+ const val LIGHT_SERVICE_TTL_MS = 10_000L
106180 fun getInstance (): PluginInventory = service()
107181
108182 /* * [IdeaPluginDescriptor.isEnabled] is deprecated; check via [PluginManagerCore.isDisabled]. */
0 commit comments