[remote development, platform] IJPL-245265 Refactor and split PluginUpdateService

GitOrigin-RevId: 8366ddc672069765b05bd32f0c262453a590a6f9
This commit is contained in:
Kostya Ripak
2026-08-11 14:33:03 +00:00
committed by intellij-monorepo-bot
parent 75fc0d8144
commit 91a5d2a2aa
38 changed files with 827 additions and 665 deletions
+1
View File
@@ -641,6 +641,7 @@ jvm_library(
"//platform/util/coroutines",
"//platform/util/coroutines:coroutines_test_lib",
"@lib//:jetbrains-annotations",
"@lib//:kotlinx-coroutines-test",
"//libraries/lz4",
"//libraries/lz4:lz4_test_lib",
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable",
@@ -1642,14 +1642,6 @@ c:com.intellij.ide.plugins.newui.PluginSearchTextField
- setSelectedItem(java.lang.String):V
- setTextIgnoreEvents(java.lang.String):V
- p:showCompletionPopup():V
c:com.intellij.ide.plugins.newui.PluginUpdatesService
- <init>():V
- calculateUpdates(java.util.function.Consumer):V
- dispose():V
- finishUpdate():V
- s:getUpdates():java.util.Collection
- s:getUpdatesTooltip():java.lang.String
- s:reapplyFilter():V
f:com.intellij.ide.plugins.newui.TagComponent
- <init>():V
- setText(java.lang.String):V
-3
View File
@@ -1357,9 +1357,6 @@ c:com.intellij.ide.plugins.newui.LinkComponent
c:com.intellij.ide.plugins.newui.PluginSearchTextField
- com.intellij.ui.SearchTextField
- <init>():V
c:com.intellij.ide.plugins.newui.PluginUpdatesService
- s:isNeedUpdate(com.intellij.ide.plugins.IdeaPluginDescriptor):Z
- recalculateUpdates():V
f:com.intellij.ide.plugins.newui.TagComponent
- com.intellij.ide.plugins.newui.LinkComponent
- <init>(java.lang.String):V
@@ -136,6 +136,7 @@
<orderEntry type="module" module-name="intellij.platform.settings" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" scope="TEST" />
<orderEntry type="library" scope="TEST" name="jetbrains-annotations" level="project" />
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-test" level="project" />
<orderEntry type="module" module-name="intellij.libraries.lz4" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.collections.immutable" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.io.storages" scope="TEST" />
@@ -12,12 +12,13 @@ import com.intellij.ide.plugins.marketplace.statistics.PluginManagerUsageCollect
import com.intellij.ide.plugins.newui.ListPluginComponent
import com.intellij.ide.plugins.newui.MultiSelectionEventHandler
import com.intellij.ide.plugins.newui.MyPluginModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.PluginDetailsPageComponent
import com.intellij.ide.plugins.newui.PluginInstallationState
import com.intellij.ide.plugins.newui.PluginLogo
import com.intellij.ide.plugins.newui.PluginModelFacade
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.PluginUpdateSubscription
import com.intellij.ide.plugins.newui.PluginsGroup
import com.intellij.ide.plugins.newui.PluginsGroupComponent
import com.intellij.ide.plugins.newui.PluginsGroupComponentWithProgress
@@ -62,7 +63,6 @@ import javax.swing.JLabel
@ApiStatus.Internal
class InstalledPluginsTab @RequiresEdt constructor(
private val pluginModelFacade: PluginModelFacade,
private val pluginUpdatesService: PluginUpdatesService,
private val coroutineScope: CoroutineScope,
private val searchInMarketplaceTabHandler: Consumer<String>?,
searchTextFieldQueryDebouncePeriodMs: Long = 100,
@@ -91,6 +91,8 @@ class InstalledPluginsTab @RequiresEdt constructor(
private val eventHandler = MultiSelectionEventHandler()
private val installedPanel = createInstalledPanel(eventHandler)
private var pluginUpdateSubscription: PluginUpdateSubscription? = null
init {
updateAllLink.isVisible = false
bundledUpdateAllLink.isVisible = false
@@ -290,9 +292,8 @@ class InstalledPluginsTab @RequiresEdt constructor(
}
}
pluginUpdatesService.calculateUpdates { updates ->
val updateModels = updates?.filter { plugin -> pluginModelFacade.isEnabled(plugin) }
?: emptyList()
pluginUpdateSubscription = PluginUpdatesService.getInstance().subscribe { updates ->
val updateModels = updates.all.filter{ plugin -> pluginModelFacade.isEnabled(plugin) }
setUpdateDescriptors(installedPanel, updateModels)
setUpdateDescriptors(searchPanel.panel, updateModels)
applyBundledUpdates(updateModels)
@@ -305,6 +306,11 @@ class InstalledPluginsTab @RequiresEdt constructor(
}
}
override fun dispose() {
pluginUpdateSubscription?.cancel()
super.dispose()
}
private fun onUpdateAllClick() {
updateAllLink.isEnabled = false
bundledUpdateAllLink.isEnabled = false
@@ -13,6 +13,7 @@ import com.intellij.ide.plugins.marketplace.statistics.PluginManagerUsageCollect
import com.intellij.ide.plugins.newui.ListPluginComponent
import com.intellij.ide.plugins.newui.MultiSelectionEventHandler
import com.intellij.ide.plugins.newui.MyPluginModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.NoOpPluginsViewCustomizer
import com.intellij.ide.plugins.newui.PluginDetailsPageComponent
import com.intellij.ide.plugins.newui.PluginInstallationState
@@ -21,7 +22,7 @@ import com.intellij.ide.plugins.newui.PluginManagerCustomizer
import com.intellij.ide.plugins.newui.PluginModelFacade
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.plugins.newui.PluginUiModelAdapter
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.PluginUpdateSubscription
import com.intellij.ide.plugins.newui.PluginsGroup
import com.intellij.ide.plugins.newui.PluginsGroupComponent
import com.intellij.ide.plugins.newui.PluginsGroupComponentWithProgress
@@ -77,13 +78,12 @@ internal class MarketplacePluginsTab @RequiresEdt constructor(
facade: PluginModelFacade,
scope: CoroutineScope,
customizer: PluginManagerCustomizer?,
service: PluginUpdatesService,
searchTextFieldQueryDebouncePeriodMs: Long = 250,
) : PluginsTab(searchTextFieldQueryDebouncePeriodMs) {
private val pluginModelFacade: PluginModelFacade = facade
private val coroutineScope: CoroutineScope = scope
private val pluginManagerCustomizer: PluginManagerCustomizer? = customizer
private val pluginUpdatesService: PluginUpdatesService = service
private var pluginUpdateSubscription: PluginUpdateSubscription? = null
private val marketplaceSortByGroup: DefaultActionGroup = DefaultActionGroup().apply {
for (option in MarketplaceTabSearchSortByOptions.entries) {
@@ -324,13 +324,8 @@ internal class MarketplacePluginsTab @RequiresEdt constructor(
marketplacePanel.doLayout()
marketplacePanel.initialSelection()
pluginUpdatesService.calculateUpdates { updates ->
val updateModels: List<PluginUiModel> = if (updates == null) {
emptyList()
}
else {
updates.filter { plugin -> pluginModelFacade.isEnabled(plugin) }
}
pluginUpdateSubscription = PluginUpdatesService.getInstance().subscribe { updates ->
val updateModels: List<PluginUiModel> = updates.all.filter { plugin -> pluginModelFacade.isEnabled(plugin) }
setUpdateDescriptors(marketplacePanel, updateModels)
setUpdateDescriptors(searchPanel.panel, updateModels)
selectionListener.accept(marketplacePanel)
@@ -675,6 +670,7 @@ internal class MarketplacePluginsTab @RequiresEdt constructor(
override fun dispose() {
marketplacePanel.dispose()
searchPanel.dispose()
pluginUpdateSubscription?.cancel()
super.dispose()
}
@@ -10,12 +10,13 @@ import com.intellij.ide.plugins.certificates.PluginCertificateManager
import com.intellij.ide.plugins.marketplace.statistics.PluginManagerUsageCollector
import com.intellij.ide.plugins.newui.ListPluginComponent
import com.intellij.ide.plugins.newui.MyPluginModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.PluginManagerCustomizer
import com.intellij.ide.plugins.newui.PluginModelAsyncOperationsExecutor
import com.intellij.ide.plugins.newui.PluginModelFacade
import com.intellij.ide.plugins.newui.PluginPriceService
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.PluginUpdateSubscription
import com.intellij.ide.plugins.newui.PluginsGroup
import com.intellij.ide.plugins.newui.PluginsGroupComponent
import com.intellij.ide.plugins.newui.PluginsTab
@@ -101,7 +102,7 @@ class PluginManagerConfigurablePanel @RequiresEdt constructor(searchQuery: Strin
private val coroutineScope: CoroutineScope
private val pluginModelFacade: PluginModelFacade
private val pluginUpdatesService: PluginUpdatesService
private val updateSubscription: PluginUpdateSubscription
private val pluginManagerCustomizer: PluginManagerCustomizer? = PluginManagerCustomizer.getInstance()
private val tabHeaderComponent: TabbedPaneHeaderComponent
@@ -131,11 +132,10 @@ class PluginManagerConfigurablePanel @RequiresEdt constructor(searchQuery: Strin
pluginModelFacade.getModel().coroutineScope = childScope
coroutineScope = childScope
pluginUpdatesService =
UiPluginManager.getInstance().subscribeToUpdatesCount(pluginModelFacade.getModel().sessionId) { updatesCount ->
coroutineScope.launch(Dispatchers.EDT + any().asContextElement()) { onPluginUpdatesRecalculation(updatesCount) }
updateSubscription =
UiPluginManager.getInstance().subscribeToPluginUpdates(pluginModelFacade.getModel().sessionId) { pluginUpdates ->
coroutineScope.launch(Dispatchers.EDT + any().asContextElement()) { onPluginUpdatesRecalculation(pluginUpdates) }
}
pluginModelFacade.getModel().pluginUpdatesService = pluginUpdatesService
CustomPluginRepositoryService.getInstance().clearCache()
@@ -285,31 +285,38 @@ class PluginManagerConfigurablePanel @RequiresEdt constructor(searchQuery: Strin
private fun resetPanels() {
CustomPluginRepositoryService.getInstance().clearCache()
marketplaceTab.resetCache()
pluginUpdatesService.recalculateUpdates()
PluginUpdatesService.getInstance().recalculateUpdates()
marketplaceTab.onPanelReset(tabHeaderComponent.getSelectionTab() == MARKETPLACE_TAB)
}
private fun onPluginUpdatesRecalculation(updatesCount: Int?) {
val count = updatesCount ?: 0
val text = Integer.toString(count)
val tooltip = PluginUpdatesService.getUpdatesTooltip()
private fun onPluginUpdatesRecalculation(pluginUpdates: List<PluginUiModel>) {
val text = Integer.toString(pluginUpdates.size)
val tooltip = getUpdatesTooltip(pluginUpdates)
tabHeaderComponent.setTabTooltip(INSTALLED_TAB, tooltip)
installedTab.onPluginUpdatesRecalculation(updatesCount, tooltip)
installedTab.onPluginUpdatesRecalculation(pluginUpdates.size, tooltip)
installedTabHeaderUpdatesCountIcon.setText(text)
tabHeaderComponent.update()
}
@Nls
fun getUpdatesTooltip(pluginUpdates: List<PluginUiModel>): @Nls String? {
if (pluginUpdates.isEmpty()) {
return null
}
return IdeBundle.message("updates.plugin.ready.tooltip",
StringUtil.join(pluginUpdates.map { it.name }, ", "),
pluginUpdates.size)
}
private fun createMarketplaceTab(): MarketplacePluginsTab {
return MarketplacePluginsTab(pluginModelFacade, coroutineScope, pluginManagerCustomizer, pluginUpdatesService)
return MarketplacePluginsTab(pluginModelFacade, coroutineScope, pluginManagerCustomizer)
}
private fun createInstalledTab(): InstalledPluginsTab {
val installedPluginsTab = InstalledPluginsTab(
pluginModelFacade,
pluginUpdatesService,
coroutineScope,
{ _ -> tabHeaderComponent.setSelectionWithEvents(MARKETPLACE_TAB) },
)
@@ -357,7 +364,7 @@ class PluginManagerConfigurablePanel @RequiresEdt constructor(searchQuery: Strin
installedTab.getInstalledSearchPanel().dispose()
pluginUpdatesService.dispose()
updateSubscription.cancel()
PluginPriceService.cancel()
pluginsState.runShutdownCallback()
@@ -2,9 +2,10 @@
package com.intellij.ide.plugins;
import com.intellij.ide.plugins.newui.PluginUpdatesService;
import com.intellij.ide.plugins.newui.PluginUpdateSubscription;
import com.intellij.ide.plugins.newui.PluginUpdatesEvent;
import com.intellij.openapi.options.ConfigurableTreeRenderer;
import com.intellij.openapi.options.UnnamedConfigurable;
import com.intellij.openapi.updateSettings.impl.InternalPluginResults;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
@@ -25,10 +26,10 @@ import java.util.function.Consumer;
* @author Alexander Lobas
*/
@ApiStatus.Internal
public final class PluginManagerConfigurableTreeRenderer extends AncestorListenerAdapter implements ConfigurableTreeRenderer, Consumer<InternalPluginResults> {
public final class PluginManagerConfigurableTreeRenderer extends AncestorListenerAdapter implements ConfigurableTreeRenderer, Consumer<PluginUpdatesEvent> {
private final CountComponent myCountLabel = new CountComponent();
private PluginUpdatesService myService;
private PluginUpdateSubscription myUpdateSubscription;
private SimpleTree myTree;
private @NlsSafe String myCountValue;
@@ -37,7 +38,7 @@ public final class PluginManagerConfigurableTreeRenderer extends AncestorListene
@Nullable UnnamedConfigurable configurable,
boolean selected) {
if (myTree == null) {
myService = PluginUpdatesService.connectWithUpdates(this);
myUpdateSubscription = PluginUpdatesService.getInstance().subscribe(this);
tree.addAncestorListener(this);
myTree = (SimpleTree)tree;
}
@@ -68,14 +69,14 @@ public final class PluginManagerConfigurableTreeRenderer extends AncestorListene
@Override
public void ancestorRemoved(AncestorEvent event) {
myService.dispose();
myUpdateSubscription.cancel();
}
@Override
public void accept(InternalPluginResults results) {
public void accept(PluginUpdatesEvent results) {
String oldCountValue = myCountValue;
int countValue = results == null ? 0 : results.getPluginUpdates().getAll().size();
myCountValue = countValue <= 0 ? null : Integer.toString(countValue);
int countValue = results == null ? 0 : results.getEnabledUpdates().size();
myCountValue = countValue == 0 ? null : Integer.toString(countValue);
if (myTree != null && !StringUtil.equals(oldCountValue, myCountValue)) {
myTree.repaint();
}
@@ -0,0 +1,111 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.newui
import com.intellij.ide.plugins.DynamicPluginEnabler
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginEnableStateChangedListener
import com.intellij.ide.plugins.PluginStateListener
import com.intellij.ide.plugins.PluginStateManager
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.updateSettings.impl.PluginUpdateHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import kotlin.time.Duration.Companion.milliseconds
@OptIn(FlowPreview::class)
@ApiStatus.Internal
class DefaultPluginUpdatesProvider(private val coroutineScope: CoroutineScope) : PluginUpdatesProvider {
private val updateMutex = Mutex()
private val flow = MutableStateFlow<PluginUpdatesEvent?>(null)
private val updateRequestFlow = MutableSharedFlow<Unit>(replay = 1, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private var lastPluginUpdates: PluginUpdatesEvent? = null
init {
PluginStateManager.addStateListener(object : PluginStateListener {
override fun install(descriptor: IdeaPluginDescriptor) {
coroutineScope.launch { dropFromLastPluginUpdates(setOf(descriptor.getPluginId())) }
}
override fun uninstall(descriptor: IdeaPluginDescriptor) {
coroutineScope.launch { dropFromLastPluginUpdates(setOf(descriptor.getPluginId())) }
}
})
DynamicPluginEnabler.addPluginStateChangedListener( object : PluginEnableStateChangedListener {
override fun stateChanged(
pluginDescriptors: Collection<IdeaPluginDescriptor>,
enable: Boolean,
) {
if (enable) {
coroutineScope.launch { update() }
return
}
coroutineScope.launch { dropFromLastPluginUpdates(pluginDescriptors.map { it.getPluginId() }.toSet()) }
}
})
collectUpdateRequests()
}
private fun collectUpdateRequests() = coroutineScope.launch(Dispatchers.IO) {
updateRequestFlow
.debounce(300.milliseconds)
.collectLatest {
updateMutex.withLock {
val model = (PluginUpdateHandler.getInstance().loadAndStorePluginUpdates(null))
val pluginUpdates = PluginUpdatesEvent(model.pluginUpdates.markLocal(),
model.disabledPluginUpdates.markLocal(),
model.updatesFromCustomRepositories.markLocal())
lastPluginUpdates = pluginUpdates
emitUpdates(pluginUpdates)
}
}
}
override suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?> {
return flow
}
override suspend fun update() {
updateRequestFlow.emit(Unit)
}
private suspend fun emitUpdates(updates: PluginUpdatesEvent) {
flow.emit(updates)
}
private suspend fun dropFromLastPluginUpdates(pluginUpdatesToRemove: Set<PluginId>) {
updateMutex.withLock {
if (lastPluginUpdates == null) return
computeUpdatesWithout(pluginUpdatesToRemove, lastPluginUpdates!!)?.also {
lastPluginUpdates = it
emitUpdates(it)
}
}
}
private fun computeUpdatesWithout(pluginIds: Set<PluginId>, updates: PluginUpdatesEvent): PluginUpdatesEvent? {
if (updates.all.none { it.pluginId in pluginIds }) {
return null
}
return PluginUpdatesEvent(
updates.enabledUpdates.filter { it.pluginId !in pluginIds },
updates.disabledUpdates.filter { it.pluginId !in pluginIds },
updates.pluginNods
)
}
private fun List<PluginDto>.markLocal(): List<PluginDto> = onEach { it.source = PluginSource.LOCAL }
}
@@ -119,10 +119,6 @@ object DefaultUiPluginManagerController : UiPluginManagerController {
return InstalledPluginsState.getInstance().installedPlugins.map { PluginUiModelAdapter(it) }.withSource()
}
override suspend fun getUpdates(): List<PluginUiModel> {
return PluginUpdatesService.getUpdates()?.map { PluginUiModelAdapter(it) }?.withSource() ?: emptyList()
}
override suspend fun getPlugin(id: PluginId): PluginUiModel? {
return PluginManagerCore.getPlugin(id)?.let { PluginUiModelAdapter(it) }?.withSource()
}
@@ -135,11 +131,6 @@ object DefaultUiPluginManagerController : UiPluginManagerController {
return PluginManagerCore.isPluginInstalled(pluginId)
}
override suspend fun isNeedUpdate(pluginId: PluginId): Boolean {
val descriptor = PluginManagerCore.getPlugin(pluginId) ?: return false
return PluginUpdatesService.isNeedUpdate(descriptor)
}
override suspend fun isBundledUpdate(pluginIds: List<PluginId>): Boolean {
val pluginIdMap = buildPluginIdMap()
return pluginIds.map { pluginIdMap[it] }.all { isBundledUpdate(it) }
@@ -534,22 +525,6 @@ object DefaultUiPluginManagerController : UiPluginManagerController {
.firstOrNull()
}
override fun connectToPluginUpdateService(sessionId: String, callback: (List<PluginUiModel>) -> Unit): PluginUpdatesService {
val session = createSession(sessionId)
if (session.updateService != null) {
val service = session.updateService!!
service.calculateUpdates({ updates -> callback(updates as List<PluginUiModel>) })
return service
} else {
val service = PluginUpdatesService.connectWithUpdates({ results ->
callback(results.pluginUpdates.all.map { it.uiModel })
})
service.setFilter { session.isPluginEnabled(it.pluginId) }
session.updateService = service
return service
}
}
override fun getAllPluginsTags(): Set<String> {
return MarketplaceRequests.getInstance().marketplaceTagsSupplier.get()
}
@@ -230,11 +230,8 @@ open class MyPluginModel(project: Project?) : InstalledPluginsTableModel(project
}
}
var pluginUpdatesService: PluginUpdatesService
get() = myPluginUpdatesService!!
set(service) {
myPluginUpdatesService = service
}
val pluginUpdatesService: PluginUpdatesService
get() = PluginUpdatesService.getInstance()
val sessionId: String
get() = mySessionId.toString()
@@ -501,7 +498,7 @@ open class MyPluginModel(project: Project?) : InstalledPluginsTableModel(project
}
}
else {
myPluginUpdatesService!!.finishUpdate()
PluginUpdatesService.getInstance().rerunCallbacks()
}
info?.indicator?.cancel()
@@ -826,7 +823,7 @@ open class MyPluginModel(project: Project?) : InstalledPluginsTableModel(project
group.titleWithEnabled(PluginModelFacade(this))
}
runInvalidFixCallback()
myPluginUpdatesService?.refreshCallbacks()
PluginUpdatesService.getInstance().rerunCallbacks()
}
override fun isDisabled(pluginId: PluginId): Boolean {
@@ -69,7 +69,6 @@ class PluginManagerSession(val sessionId: String) {
val uninstalledPlugins: MutableSet<PluginId> = ConcurrentCollectionFactory.createConcurrentSet()
val pluginStates: MutableMap<PluginId, PluginEnabledState?> = mutableMapOf()
val statesDiff: MutableMap<IdeaPluginDescriptor, Pair<PluginEnableDisableAction, PluginEnabledState>> = ConcurrentHashMap()
var updateService: PluginUpdatesService? = null
var needRestart = false
fun isPluginDisabled(pluginId: PluginId): Boolean = !isPluginEnabled(pluginId)
@@ -11,7 +11,6 @@ import com.intellij.openapi.application.ex.ApplicationInfoEx
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.project.Project
import com.intellij.openapi.updateSettings.impl.PluginDownloader
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.platform.ide.CoreUiCoroutineScopeHolder
import kotlinx.coroutines.CoroutineScope
@@ -65,7 +64,7 @@ internal object PluginModelAsyncOperationsExecutor {
suspend fun loadUpdates(): List<PluginUiModel> {
return withContext(Dispatchers.IO) {
UiPluginManager.getInstance().getUpdateModels()
PluginUpdatesService.getInstance().awaitUpdates().toList()
}
}
@@ -145,10 +144,10 @@ internal object PluginModelAsyncOperationsExecutor {
}
}
fun findPlugins(downloaders: Collection<PluginDownloader>, callback: Function<Map<PluginId, PluginUiModel>, Unit>) {
fun findPlugins(pluginIds: Collection<PluginId>, callback: Function<Map<PluginId, PluginUiModel>, Unit>) {
val coroutineScope = service<CoreUiCoroutineScopeHolder>().coroutineScope
coroutineScope.launch(Dispatchers.IO) {
val pluginModels = UiPluginManager.getInstance().findInstalledPlugins(downloaders.map(PluginDownloader::id).toSet())
val pluginModels = UiPluginManager.getInstance().findInstalledPlugins(pluginIds.toSet())
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
callback.apply(pluginModels)
}
@@ -0,0 +1,39 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.newui
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.openapi.extensions.ExtensionPointName
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
/**
* Interface responsible for providing plugin update events and managing the update process.
*
* This interface is intended for internal use and allows clients to subscribe to a flow of plugin update events
* or trigger the update mechanism for plugins.
*
* Currently, there are two implementations of this interface: {@link DefaultPluginUpdatesProvider} in monolith and frontend
* and {@link BackendPluginUpdatesProvider} in frontend-split mode.
*/
@ApiStatus.Internal
interface PluginUpdatesProvider {
suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?>
suspend fun update()
companion object {
val EP_NAME: ExtensionPointName<PluginUpdatesProvider> = ExtensionPointName.create("com.intellij.pluginUpdatesProvider")
@JvmStatic
fun getInstances(): Collection<PluginUpdatesProvider> = EP_NAME.extensionList
}
}
/**
* Represents an event triggered when plugin updates occur, is a mirror of {@link InternalPluginResults}
*/
@ApiStatus.Internal
@Serializable
data class PluginUpdatesEvent(val enabledUpdates: List<PluginDto>, val disabledUpdates: List<PluginDto>, val pluginNods: List<PluginDto>) {
val all: Collection<PluginDto> by lazy { enabledUpdates + disabledUpdates }
}
@@ -1,310 +1,189 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.newui;
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.newui
import com.intellij.ide.IdeBundle;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.InstalledPluginsState;
import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.ide.plugins.PluginStateListener;
import com.intellij.ide.plugins.PluginStateManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.updateSettings.impl.InternalPluginResults;
import com.intellij.openapi.updateSettings.impl.PluginDownloader;
import com.intellij.openapi.updateSettings.impl.PluginUpdates;
import com.intellij.openapi.updateSettings.impl.UpdateCheckerFacade;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.concurrency.NonUrgentExecutor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.UI
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.function.Consumer
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.time.Duration.Companion.milliseconds
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@ApiStatus.Internal
fun interface PluginUpdateSubscription {
fun cancel()
}
/**
* @author Alexander Lobas
*/
public class PluginUpdatesService {
private static final Logger LOG = Logger.getInstance(PluginUpdatesService.class);
private static final List<PluginUpdatesService> SERVICES = new ArrayList<>();
private static final Object ourLock = new Object();
private static final @NotNull Condition<IdeaPluginDescriptor> DEFAULT_FILTER = // only enabled plugins by default
descriptor -> !PluginManagerCore.isDisabled(descriptor.getPluginId());
typealias PluginUpdateCallback = Consumer<PluginUpdatesEvent>
// FIXME it is strange that users of this class need to known which updates came from custom repositories (IJPL-6087)
/** clients should receive filtered updates by default */
private static @NotNull InternalPluginResults ourAllUpdates = InternalPluginResults.empty();
private static @NotNull Condition<? super IdeaPluginDescriptor> ourFilter = DEFAULT_FILTER;
private static boolean ourPrepared;
private static boolean ourPreparing;
private static boolean ourReset;
@ApiStatus.Internal
@Service
@OptIn(FlowPreview::class, ExperimentalAtomicApi::class)
class PluginUpdatesService(val coroutineScope: CoroutineScope) {
private final List<Consumer<@NotNull InternalPluginResults>> myUpdateCallbacks = new ArrayList<>();
private boolean mySetFilter;
private val myCallbacks = CopyOnWriteArrayList<PluginUpdateCallback>()
private val pluginUpdateFlow = MutableStateFlow<PluginUpdatesEvent?>(null)
private val updateIdsFlow = MutableStateFlow<Set<PluginId>?>(null)
private val updateRequestFlow = MutableSharedFlow<Unit>(replay = 1, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val providerSnapshots = ConcurrentHashMap<PluginUpdatesProvider, PluginUpdatesEvent>()
static {
PluginStateManager.addStateListener(new PluginStateListener() {
@Override
public void install(@NotNull IdeaPluginDescriptor descriptor) {
finishUpdate(descriptor);
}
companion object {
private val LOG = logger<PluginUpdatesService>()
@Override
public void uninstall(@NotNull IdeaPluginDescriptor descriptor) {
finishUpdate(descriptor);
}
});
}
@JvmStatic
fun getInstance(): PluginUpdatesService = service()
@ApiStatus.Internal
public static @NotNull PluginUpdatesService connectWithUpdates(@NotNull Consumer<@NotNull InternalPluginResults> callback) {
PluginUpdatesService service = new PluginUpdatesService();
service.myUpdateCallbacks.add(callback);
synchronized (ourLock) {
SERVICES.add(service);
if (ourPrepared) {
callback.accept(getFilteredUpdateResult());
return service;
}
}
calculateUpdates();
return service;
}
private static @NotNull InternalPluginResults getFilteredUpdateResult() {
synchronized (ourLock) {
if (ourAllUpdates.isEmpty()) {
return ourAllUpdates;
}
final var filter = ourFilter;
return new InternalPluginResults(
new PluginUpdates(
ContainerUtil.filter(ourAllUpdates.getPluginUpdates().getAllEnabled(), d -> filter.test(d.getDescriptor())),
ContainerUtil.filter(ourAllUpdates.getPluginUpdates().getAllDisabled(), d -> filter.test(d.getDescriptor())),
ourAllUpdates.getPluginUpdates().getIncompatible()
),
ourAllUpdates.getPluginNods(),
ourAllUpdates.getErrors()
);
@JvmStatic
fun isNeedUpdate(pluginId: PluginId): Boolean {
return runBlockingMaybeCancellable { getInstance().awaitHasUpdate(pluginId) }
}
}
public void calculateUpdates(@NotNull Consumer<? super Collection<PluginUiModel>> callback) {
synchronized (ourLock) {
final var adaptedCallback = adaptDescriptorConsumerToUpdateResultConsumer(callback);
myUpdateCallbacks.add(adaptedCallback);
if (ourPrepared) {
adaptedCallback.accept(getFilteredUpdateResult());
return;
}
}
calculateUpdates();
init {
startUpdateCollection()
startUpdateTrigger()
}
private static void finishUpdate(@NotNull IdeaPluginDescriptor descriptor) {
synchronized (ourLock) {
if (!ourPrepared) {
return;
private suspend fun ensureUpdatesStarted() {
if (pluginUpdateFlow.value == null) {
triggerUpdates()
}
}
private fun startUpdateCollection() {
for (provider in PluginUpdatesProvider.getInstances()) {
coroutineScope.launch {
provider.pluginUpdateEvents()
.catch { e -> LOG.warn("Plugin update provider failed: ${provider.javaClass.name}", e) }
.collect { event ->
event?.let {
providerSnapshots[provider] = event
onProviderUpdated()
}
}
}
}
}
private suspend fun onProviderUpdated() {
val merged = mergeUpdates(providerSnapshots.values)
pluginUpdateFlow.value = merged
updateIdsFlow.value = merged.all.mapTo(HashSet()) { plugin -> plugin.pluginId }
withContext(Dispatchers.UI + ModalityState.any().asContextElement()) {
dispatchCallbacks(merged)
}
}
private fun startUpdateTrigger() = coroutineScope.launch {
updateRequestFlow
.debounce(300.milliseconds)
.collect {
PluginUpdatesProvider.getInstances().forEach { it.update() }
}
boolean removed = removeUpdate(descriptor.getPluginId());
if (removed) {
var results = getFilteredUpdateResult();
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(results);
}
private suspend fun triggerUpdates() {
updateRequestFlow.emit(Unit)
}
/**
* Registers a [callback] to receive plugin update events and returns a [PluginUpdateSubscription] to cancel it.
*
* By default, the [callback] is invoked on [Dispatchers.UI][com.intellij.openapi.application.UI], WIL not allowed.
*
* Note: if an update snapshot is already available, the [callback] is also invoked once synchronously on the
* calling thread of [subscribe] with that snapshot; all later invocations happen on [Dispatchers.UI].
*/
@RequiresEdt
fun subscribe(@RequiresEdt callback: PluginUpdateCallback): PluginUpdateSubscription {
myCallbacks.add(callback)
val currentSnapshot = getLastUpdates()
if (currentSnapshot != null) {
callback.accept(currentSnapshot)
} else {
recalculateUpdates()
}
return PluginUpdateSubscription {
myCallbacks.remove(callback)
}
}
fun recalculateUpdates(): Job = coroutineScope.launch { triggerUpdates() }
suspend fun awaitUpdates(): Collection<PluginUiModel> {
ensureUpdatesStarted()
return pluginUpdateFlow.filterNotNull().first().all
}
@VisibleForTesting
fun flow(): Flow<PluginUpdatesEvent?> = pluginUpdateFlow
suspend fun awaitHasUpdate(pluginId: PluginId): Boolean {
ensureUpdatesStarted()
return updateIdsFlow.filterNotNull().first().contains(pluginId)
}
@RequiresEdt
fun rerunCallbacks() {
val currentUpdates = getLastUpdates()
if (currentUpdates != null) {
dispatchCallbacks(currentUpdates)
}
}
private fun getLastUpdates(): PluginUpdatesEvent? {
return pluginUpdateFlow.value
}
private fun dispatchCallbacks(updates: PluginUpdatesEvent) {
myCallbacks.forEach { it.accept(updates) }
}
@VisibleForTesting
internal fun mergeUpdates(updateEvents: Collection<PluginUpdatesEvent>): PluginUpdatesEvent =
PluginUpdatesEvent(mergePlugins(updateEvents) { it.enabledUpdates },
mergePlugins(updateEvents) { it.disabledUpdates },
mergePlugins(updateEvents) { it.pluginNods })
private fun mergePlugins(updateEvents: Collection<PluginUpdatesEvent>, updates: (PluginUpdatesEvent) -> List<PluginDto>): List<PluginDto> {
val merged = LinkedHashMap<PluginId, PluginDto>()
for (event in updateEvents) {
for (plugin in updates(event)) {
val existing = merged[plugin.pluginId]
if (existing == null) {
merged[plugin.pluginId] = plugin
}
else {
existing.source = existing.source.addSource(plugin.source)
}
}
}
}
private static boolean removeUpdate(@NotNull PluginId pluginId) {
if (!ContainerUtil.exists(ourAllUpdates.getPluginUpdates().getAll(), d -> Objects.equals(d.getDescriptor().getPluginId(), pluginId))) {
return false;
}
ourAllUpdates = new InternalPluginResults(
new PluginUpdates(
ContainerUtil.filter(ourAllUpdates.getPluginUpdates().getAllEnabled(), d -> !Objects.equals(d.getDescriptor().getPluginId(), pluginId)),
ContainerUtil.filter(ourAllUpdates.getPluginUpdates().getAllDisabled(), d -> !Objects.equals(d.getDescriptor().getPluginId(), pluginId)),
ourAllUpdates.getPluginUpdates().getIncompatible()
),
ourAllUpdates.getPluginNods(),
ourAllUpdates.getErrors()
);
return true;
}
public void finishUpdate() {
synchronized (ourLock) {
if (!ourPrepared) {
return;
}
var results = getFilteredUpdateResult();
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(results);
}
}
}
public void recalculateUpdates() {
synchronized (ourLock) {
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(InternalPluginResults.empty());
}
if (ourPreparing) {
resetUpdates();
}
else {
calculateUpdates();
}
}
}
private static void resetUpdates() {
ourReset = true;
}
@ApiStatus.Internal
public void setFilter(@NotNull Condition<? super IdeaPluginDescriptor> filter) {
synchronized (ourLock) {
if (!mySetFilter && ourFilter != DEFAULT_FILTER) {
LOG.warn("Filter already set to " + ourFilter + ", new filter " + filter + " will be ignored", new Throwable());
return;
}
mySetFilter = true;
setOurFilter(filter);
}
}
private static void setOurFilter(@NotNull Condition<? super IdeaPluginDescriptor> filter) {
ourFilter = filter;
reapplyFilter();
}
public static void reapplyFilter() {
synchronized (ourLock) {
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(InternalPluginResults.empty());
}
final var filteredUpdates = getFilteredUpdateResult();
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(filteredUpdates);
}
}
}
@ApiStatus.Internal
public void refreshCallbacks() {
reapplyFilter();
}
public void dispose() {
synchronized (ourLock) {
dispose(this);
myUpdateCallbacks.clear();
if (mySetFilter) {
setOurFilter(DEFAULT_FILTER);
mySetFilter = false;
}
}
}
private static void dispose(@NotNull PluginUpdatesService service) {
synchronized (ourLock) {
SERVICES.remove(service);
if (SERVICES.isEmpty()) {
ourAllUpdates = InternalPluginResults.empty();
ourPrepared = false;
ourPreparing = false;
}
}
}
public static boolean isNeedUpdate(@NotNull IdeaPluginDescriptor descriptor) {
PluginId pluginId = descriptor.getPluginId();
synchronized (ourLock) {
if (ourPrepared) {
final var filteredUpdates = getFilteredUpdateResult();
assert filteredUpdates != null;
return ContainerUtil.exists(filteredUpdates.getPluginUpdates().getAll(), d -> Objects.equals(d.getDescriptor().getPluginId(), pluginId));
}
}
return InstalledPluginsState.getInstance().hasNewerVersion(pluginId);
}
public static @Unmodifiable Collection<IdeaPluginDescriptor> getUpdates() {
synchronized (ourLock) {
if (!ourPrepared || ourPreparing) {
return List.of();
}
final var filteredUpdates = getFilteredUpdateResult();
return ContainerUtil.map(filteredUpdates.getPluginUpdates().getAll(), PluginDownloader::getDescriptor);
}
}
public static @Nullable @Nls String getUpdatesTooltip() {
Collection<IdeaPluginDescriptor> updates = getUpdates();
if (ContainerUtil.isEmpty(updates)) {
return null;
}
return IdeBundle.message("updates.plugin.ready.tooltip", StringUtil.join(updates, plugin -> plugin.getName(), ", "), updates.size());
}
private static void calculateUpdates() {
synchronized (ourLock) {
if (ourPreparing) {
return;
}
ourPreparing = true;
ourAllUpdates = InternalPluginResults.empty();
}
// for example, if executed as part of Traverse UI - don't wait check updates
if (ApplicationManager.getApplication().isHeadlessEnvironment()) {
return;
}
NonUrgentExecutor.getInstance().execute(() -> {
InternalPluginResults updates = UpdateCheckerFacade.getInstance().checkInstalledPluginUpdates(null, null);
ApplicationManager.getApplication().invokeLater(() -> {
synchronized (ourLock) {
ourPreparing = false;
if (ourReset) {
ourReset = false;
calculateUpdates();
return;
}
ourPrepared = true;
ourAllUpdates = updates;
final var filteredUpdates = getFilteredUpdateResult();
for (PluginUpdatesService service : SERVICES) {
service.runAllCallbacks(filteredUpdates);
}
}
}, ModalityState.any());
});
}
private void runAllCallbacks(@NotNull InternalPluginResults filteredUpdates) {
for (var callback : myUpdateCallbacks) {
callback.accept(filteredUpdates);
}
}
private static @NotNull Consumer<InternalPluginResults> adaptDescriptorConsumerToUpdateResultConsumer(
@NotNull Consumer<? super Collection<PluginUiModel>> consumer
) {
return updateResult -> {
assert updateResult != null;
consumer.accept(ContainerUtil.map(updateResult.getPluginUpdates().getAll(), downloader -> downloader.getUiModel()));
};
return merged.values.toList()
}
}
@@ -63,10 +63,6 @@ class UiPluginManager {
return getController().getInstalledPlugins()
}
suspend fun getUpdateModels(): List<PluginUiModel> {
return getController().getUpdates()
}
suspend fun loadPluginDetails(model: PluginUiModel): PluginUiModel? {
return getController().loadPluginDetails(model)
}
@@ -236,7 +232,7 @@ class UiPluginManager {
@RequiresBackgroundThread(generateAssertion = false)
fun isNeedUpdate(pluginId: PluginId): Boolean {
return runBlockingMaybeCancellable { getController().isNeedUpdate(pluginId) }
return runBlockingMaybeCancellable { PluginUpdatesService.getInstance().awaitHasUpdate(pluginId) }
}
suspend fun getPluginInstallationState(pluginId: PluginId): PluginInstallationState {
@@ -250,8 +246,9 @@ class UiPluginManager {
return DefaultUiPluginManagerController
}
fun subscribeToUpdatesCount(sessionId: String, callback: (Int?) -> Unit): PluginUpdatesService {
return getController().connectToPluginUpdateService(sessionId, { updatedPlugins -> callback(updatedPlugins?.size ?: 0)})
fun subscribeToPluginUpdates(sessionId: String, callback: (List<PluginUiModel>) -> Unit): PluginUpdateSubscription {
val session = PluginManagerSessionService.getInstance().createSession(sessionId)
return PluginUpdatesService.getInstance().subscribe { updatedPlugins -> callback(updatedPlugins.all.filter { session.isPluginEnabled(it.pluginId) }) }
}
companion object {
@@ -32,7 +32,6 @@ interface UiPluginManagerController {
suspend fun getVisiblePlugins(showImplementationDetails: Boolean): List<PluginUiModel>
suspend fun initSession(sessionId: String): InitSessionResult
suspend fun getInstalledPlugins(): List<PluginUiModel>
suspend fun getUpdates(): List<PluginUiModel>
suspend fun executePluginsSearch(query: String, count: Int, includeIncompatible: Boolean): PluginSearchResult
suspend fun loadPluginDetails(model: PluginUiModel): PluginUiModel?
suspend fun loadPluginReviews(pluginId: PluginId, page: Int): List<PluginReviewComment>?
@@ -57,7 +56,6 @@ interface UiPluginManagerController {
suspend fun getLastCompatiblePluginUpdateModel(pluginId: PluginId, buildNumber: String? = null, indicator: ProgressIndicator? = null): PluginUiModel?
suspend fun getLastCompatiblePluginUpdate(allIds: Set<PluginId>, throwExceptions: Boolean, buildNumber: String? = null): List<IdeCompatibleUpdate>
suspend fun updateDescriptorsForInstalledPlugins()
suspend fun isNeedUpdate(pluginId: PluginId): Boolean
suspend fun getPluginInstallationState(pluginId: PluginId): PluginInstallationState
suspend fun getPluginInstallationStates(): Map<PluginId, PluginInstallationState>
suspend fun checkPluginCanBeDownloaded(pluginUiModel: PluginUiModel, progressIndicator: ProgressIndicator?): Boolean
@@ -72,7 +70,6 @@ interface UiPluginManagerController {
fun filterPluginsRequiringUltimateButItsDisabled(pluginIds: List<PluginId>): List<PluginId>
fun getAllPluginsTags(): Set<String>
fun getAllVendors(): Set<String>
fun connectToPluginUpdateService(sessionId: String, callback: (List<PluginUiModel>) -> Unit): PluginUpdatesService
suspend fun loadErrors(sessionId: String): Map<PluginId, CheckErrorsResult>
suspend fun loadErrors(sessionId: String, pluginIds: List<PluginId>): Map<PluginId, CheckErrorsResult>
@@ -7,7 +7,6 @@ import com.intellij.ide.plugins.newui.MyPluginModel
import com.intellij.ide.plugins.newui.PluginDetailsPageComponent
import com.intellij.ide.plugins.newui.PluginModelFacade
import com.intellij.ide.plugins.newui.PluginUiModelAdapter
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
@@ -41,9 +40,6 @@ internal class DetectedPluginsPanel(project: Project?) : OrderPanel<PluginDownlo
init {
val pluginModel = MyPluginModel(project)
pluginModel.pluginUpdatesService = object : PluginUpdatesService() {
override fun finishUpdate() {}
}
myDetailsComponent = PluginDetailsPageComponent(PluginModelFacade(pluginModel), LinkListener { _, _ -> }, true)
val entryTable = getEntryTable()
entryTable.setTableHeader(null)
@@ -18,7 +18,6 @@ import com.intellij.ide.plugins.newui.PluginDetailsPageComponent;
import com.intellij.ide.plugins.newui.PluginModelFacade;
import com.intellij.ide.plugins.newui.PluginUiModel;
import com.intellij.ide.plugins.newui.PluginUiModelAdapter;
import com.intellij.ide.plugins.newui.PluginUpdatesService;
import com.intellij.ide.plugins.newui.PluginsGroup;
import com.intellij.ide.plugins.newui.PluginsGroupComponent;
import com.intellij.ide.plugins.newui.UiPluginManager;
@@ -117,10 +116,6 @@ public class PluginUpdateDialog extends DialogWrapper {
};
myPluginModel.setTopController(Configurable.TopComponentController.EMPTY);
myPluginModel.setPluginUpdatesService(new PluginUpdatesService() {
@Override
public void finishUpdate() { }
});
//noinspection unchecked
myDetailsPage = new PluginDetailsPageComponent(new PluginModelFacade(myPluginModel),
@@ -177,33 +172,22 @@ public class PluginUpdateDialog extends DialogWrapper {
setTitle(IdeBundle.message("dialog.title.plugin.updates"));
}
public static boolean showDialogAndUpdate(@NotNull Collection<PluginDownloader> downloaders, @NotNull PluginUpdateDialog dialog) {
public static boolean showDialogAndUpdateDownloaders(@NotNull Collection<PluginDownloader> downloaders, @NotNull PluginUpdateDialog dialog) {
if (dialog.showAndGet()) {
List<PluginUiModel> selectedPlugins = dialog.getSelectedPluginModels();
List<PluginDownloader> selectedDownloaders = findDownloadersForPlugins(downloaders, selectedPlugins);
Set<PluginId> selectedPlugins = ContainerUtil.map2Set(dialog.getSelectedPluginModels(), PluginUiModel::getPluginId);
List<PluginDownloader> selectedDownloaders = ContainerUtil.filter(downloaders, downloader -> selectedPlugins.contains(downloader.getId()));
runUpdateAll(selectedDownloaders, dialog.getContentPanel(), dialog.myFinishCallback, null);
return true;
}
return false;
}
public static List<PluginDownloader> getSelectedDownloaders(@NotNull Collection<PluginDownloader> downloaders,
@NotNull PluginUpdateDialog dialog) {
return findDownloadersForPlugins(downloaders, dialog.getSelectedPluginModels());
}
private static @NotNull List<PluginDownloader> findDownloadersForPlugins(@NotNull Collection<PluginDownloader> downloaders,
@NotNull List<PluginUiModel> selectedPlugins) {
List<PluginDownloader> selectedDownloaders = new ArrayList<>();
Set<PluginId> selectedPluginIds = ContainerUtil.map2Set(selectedPlugins, PluginUiModel::getPluginId);
for (PluginDownloader downloader : downloaders) {
if (selectedPluginIds.contains(downloader.getDescriptor().getPluginId())) {
selectedDownloaders.add(downloader);
}
public static boolean showDialogAndUpdate(@NotNull PluginUpdateDialog dialog) {
if (dialog.showAndGet()) {
PluginUpdateHandler.installUpdates(dialog.getSelectedPluginModels(), dialog.getContentPanel(), dialog.myFinishCallback, null);
return true;
}
return selectedDownloaders;
return false;
}
protected void doIgnoreUpdateAction(ActionEvent e) {
@@ -4,10 +4,11 @@ package com.intellij.openapi.updateSettings.impl
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.jetbrains.annotations.ApiStatus
import java.util.UUID
import java.util.function.Consumer
import javax.swing.JComponent
// Class used for loading and installing plugin updates on the backend and frontend.
@@ -15,20 +16,39 @@ import javax.swing.JComponent
// Eventually should be responsible for all plugin updates.
@ApiStatus.Internal
interface PluginUpdateHandler {
suspend fun loadAndStorePluginUpdates(buildNumber: String?, sessionId: String = UUID.randomUUID().toString(), indicator: ProgressIndicator? = null): PluginUpdatesModel
suspend fun installUpdates(sessionId: String, updates: List<PluginUiModel>, component: JComponent?, finishCallback: Runnable?)
suspend fun loadAndStorePluginUpdates(buildNumber: String?, indicator: ProgressIndicator? = null): PluginUpdatesModel
suspend fun installUpdates(updates: Collection<PluginUiModel>, component: JComponent?, finishCallback: Runnable?, customRestarter: Consumer<Boolean>? = null)
suspend fun ignorePluginUpdates(sessionId: String)
suspend fun ignorePluginUpdates()
companion object {
@JvmStatic
fun getInstance(): PluginUpdateHandler = PluginUpdateHandlerProvider.getInstance().getPluginUpdateHandler()
@JvmStatic
fun installUpdates(
updates: Collection<PluginUiModel>,
component: JComponent?,
finishCallback: Runnable?,
customRestarter: Consumer<Boolean>? = null,
) {
runBlockingMaybeCancellable {
getInstance().installUpdates(updates, component, finishCallback, customRestarter)
}
}
@JvmStatic
fun loadAndStorePluginUpdates(buildNumber: String?, indicator: ProgressIndicator? = null): PluginUpdatesModel {
return runBlockingMaybeCancellable {
getInstance().loadAndStorePluginUpdates(buildNumber, indicator)
}
}
}
}
@ApiStatus.Internal
@Serializable
data class PluginUpdatesModel(
val sessionId: String,
val pluginUpdates: List<PluginDto>,
val disabledPluginUpdates: List<PluginDto> = emptyList(),
val updatesFromCustomRepositories: List<PluginDto>,
@@ -5,6 +5,7 @@ import com.intellij.ide.IdeBundle;
import com.intellij.ide.plugins.CountComponent;
import com.intellij.ide.plugins.PluginManagerConfigurable;
import com.intellij.ide.plugins.newui.PluginUpdatesService;
import com.intellij.ide.plugins.newui.PluginUpdateSubscription;
import com.intellij.ide.plugins.newui.TabbedPaneHeaderComponent;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Disposer;
@@ -31,7 +32,7 @@ public final class PluginsTabFactory implements WelcomeTabFactory {
}
private static final class MyDefaultWelcomeScreenTab extends TabbedWelcomeScreen.DefaultWelcomeScreenTab {
private final PluginUpdatesService myService;
private final PluginUpdateSubscription myPluginUpdateSubscription;
private final CountComponent myCountLabel = new CountComponent();
private JComponent myParent;
private final Disposable parentDisposable;
@@ -44,9 +45,9 @@ public final class PluginsTabFactory implements WelcomeTabFactory {
myKeyComponent.add(myCountLabel, BorderLayout.EAST);
myCountLabel.setVisible(false);
myService = PluginUpdatesService.connectWithUpdates(results -> {
int countValue = results.getPluginUpdates().getAll().size();
@NlsSafe String text = countValue <= 0 ? null : Integer.toString(countValue);
myPluginUpdateSubscription = PluginUpdatesService.getInstance().subscribe(results -> {
int countValue = results.getEnabledUpdates().size();
@NlsSafe String text = countValue == 0 ? null : Integer.toString(countValue);
myCountLabel.setText(text);
myCountLabel.setVisible(text != null);
if (myParent != null) {
@@ -61,8 +62,8 @@ public final class PluginsTabFactory implements WelcomeTabFactory {
parent.addAncestorListener(new AncestorListenerAdapter() {
@Override
public void ancestorRemoved(AncestorEvent event) {
if (myService != null) {
myService.dispose();
if (myPluginUpdateSubscription != null) {
myPluginUpdateSubscription.cancel();
}
}
});
@@ -0,0 +1,261 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.newui
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.Disposable
import com.intellij.platform.util.coroutines.childScope
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.common.waitUntil
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class)
@TestApplication
internal class PluginUpdatesServiceTest {
@Test
fun `merge updates deduplicates plugin ids independently in each bucket`() {
val mergedUpdates = mergeUpdates(
PluginUpdatesEvent(enabledUpdates = listOf(pluginDto("enabled.local", PluginSource.LOCAL), pluginDto("enabled.both", PluginSource.LOCAL)),
disabledUpdates = listOf(pluginDto("disabled.local", PluginSource.LOCAL), pluginDto("disabled.both", PluginSource.LOCAL)),
pluginNods = listOf(pluginDto("custom.local", PluginSource.LOCAL), pluginDto("custom.both", PluginSource.LOCAL))),
PluginUpdatesEvent(enabledUpdates = listOf(pluginDto("enabled.both", PluginSource.REMOTE), pluginDto("enabled.remote", PluginSource.REMOTE)),
disabledUpdates = listOf(pluginDto("disabled.both", PluginSource.REMOTE), pluginDto("disabled.remote", PluginSource.REMOTE)),
pluginNods = listOf(pluginDto("custom.both", PluginSource.REMOTE), pluginDto("custom.remote", PluginSource.REMOTE))),
)
assertMergedPlugins(mergedUpdates.enabledUpdates, "enabled.local", "enabled.both", "enabled.remote")
assertMergedPlugins(mergedUpdates.disabledUpdates, "disabled.local", "disabled.both", "disabled.remote")
assertMergedPlugins(mergedUpdates.pluginNods, "custom.local", "custom.both", "custom.remote")
}
private fun mergeUpdates(first: PluginUpdatesEvent, second: PluginUpdatesEvent): PluginUpdatesEvent {
return PluginUpdatesService(CoroutineScope(EmptyCoroutineContext)).mergeUpdates(listOf(first, second))
}
private fun assertMergedPlugins(plugins: List<PluginDto>, vararg pluginIds: String) {
assertThat(plugins.map { it.pluginId.idString }).containsExactly(*pluginIds)
assertThat(plugins.map { it.source }).containsExactly(PluginSource.LOCAL, PluginSource.BOTH, PluginSource.REMOTE)
}
private fun pluginDto(id: String, source: PluginSource): PluginDto =
PluginDto(name = id, pluginId = PluginId.getId(id)).apply { this.source = source }
@Test
fun `a failing provider flow must not stop updates from other providers`(@TestDisposable disposable: Disposable): Unit =
runTest {
val healthy = MutableStateFlow<PluginUpdatesEvent?>(null)
val failing = flow<PluginUpdatesEvent?> {
throw RuntimeException("provider flow failed")
}
ExtensionTestUtil.maskExtensions(
PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(healthy), FakeProvider(failing)), disposable,
)
val service = PluginUpdatesService(backgroundScope)
// A healthy provider now reports an update; it must still reach the subscriber.
healthy.value = PluginUpdatesEvent(listOf(pluginDto("plugin.healthy", PluginSource.LOCAL)), emptyList(), emptyList())
advanceUntilIdle()
val received = withTimeout(1.seconds) { service.awaitUpdates() }
assertThat(received).anySatisfy {
assertThat( it.pluginId.idString == "plugin.healthy").isTrue()
}
}
@Test
fun `a provider that never emits must not block updates from other providers`(@TestDisposable disposable: Disposable): Unit =
runTest {
val healthy = MutableStateFlow<PluginUpdatesEvent?>(null)
val silent = MutableSharedFlow<PluginUpdatesEvent>() // never emits
ExtensionTestUtil.maskExtensions(
PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(healthy), FakeProvider(silent)), disposable,
)
val service = PluginUpdatesService(backgroundScope)
healthy.value = PluginUpdatesEvent(listOf(pluginDto("plugin.healthy", PluginSource.LOCAL)), emptyList(), emptyList())
advanceUntilIdle()
val updates = withTimeout(1.seconds) { service.awaitUpdates() }
assertThat(updates).anySatisfy {
assertThat( it.pluginId.idString == "plugin.healthy").isTrue()
}
}
@Test
fun `subscribe delivers updates and late subscriber also receives updates`(@TestDisposable disposable: Disposable): Unit =
timeoutRunBlocking {
val events = MutableStateFlow<PluginUpdatesEvent?>(null)
ExtensionTestUtil.maskExtensions(PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(events)), disposable)
val serviceScope = childScope("PluginUpdatesServiceTest", Dispatchers.Default)
try {
val service = PluginUpdatesService(serviceScope)
val earlyReceiver = CopyOnWriteArrayList<PluginUpdatesEvent>()
service.subscribe { earlyReceiver.add(it) }
events.value = PluginUpdatesEvent(listOf(pluginDto("plugin.first", PluginSource.LOCAL)), emptyList(), emptyList())
waitUntil("early subscriber receives the plugin.first update", 1.seconds) {
earlyReceiver.hasUpdateFor("plugin.first")
}
assertThat(earlyReceiver)
.describedAs("early subscriber receives exactly one update once it is published")
.hasSize(1)
assertThat(earlyReceiver.single().all.map { it.pluginId.idString })
.describedAs("the update delivered to the early subscriber is for plugin.first")
.containsExactly("plugin.first")
val lateReceived = CopyOnWriteArrayList<PluginUpdatesEvent>()
service.subscribe { lateReceived.add(it) }
assertThat(lateReceived)
.describedAs("late subscriber immediately receives the current snapshot on subscribe")
.hasSize(1)
assertThat(lateReceived.single().all.map { it.pluginId.idString })
.describedAs("late subscriber receives the same plugin.first update")
.containsExactly("plugin.first")
}
finally {
serviceScope.cancel()
}
}
@Test
fun `awaitUpdates and awaitHasUpdate reflect available updates`(@TestDisposable disposable: Disposable): Unit =
runTest {
val events = MutableStateFlow(eventWith("plugin.available"))
ExtensionTestUtil.maskExtensions(PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(events)), disposable)
val service = PluginUpdatesService(backgroundScope)
advanceUntilIdle()
val updates = withTimeout(1.seconds) { service.awaitUpdates() }
assertThat(updates.map { it.pluginId.idString }).contains("plugin.available")
assertThat(service.awaitHasUpdate(PluginId.getId("plugin.available")))
.describedAs("awaitHasUpdate is true for an available update").isTrue()
assertThat(service.awaitHasUpdate(PluginId.getId("plugin.absent")))
.describedAs("awaitHasUpdate is false for an unknown plugin").isFalse()
}
@Test
fun `rerunCallbacks re-delivers the last snapshot to callbacks`(@TestDisposable disposable: Disposable): Unit =
timeoutRunBlocking {
val events = MutableStateFlow(eventWith("plugin.snapshot"))
ExtensionTestUtil.maskExtensions(PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(events)), disposable)
val serviceScope = childScope("PluginUpdatesServiceTest", Dispatchers.Default)
try {
val received = CopyOnWriteArrayList<PluginUpdatesEvent>()
val service = PluginUpdatesService(serviceScope)
service.subscribe { received.add(it) }
waitUntil("subscriber receives the initial snapshot", 1.seconds) { received.hasUpdateFor("plugin.snapshot") }
received.clear()
service.rerunCallbacks()
waitUntil("rerunCallbacks re-delivers the last snapshot", 1.seconds) { received.hasUpdateFor("plugin.snapshot") }
}
finally {
serviceScope.cancel()
}
}
@Test
fun `recalculateUpdates re-runs provider update checks`(@TestDisposable disposable: Disposable): Unit =
runTest {
val nextResult = AtomicReference(emptyEvent())
val events = MutableStateFlow<PluginUpdatesEvent?>(null)
val provider = object : PluginUpdatesProvider {
override suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?> = events
override suspend fun update() {
events.value = nextResult.get()
}
}
ExtensionTestUtil.maskExtensions(PluginUpdatesProvider.EP_NAME, listOf(provider), disposable)
val service = PluginUpdatesService(backgroundScope)
// recalculateUpdates() runs the provider's update(), which publishes the staged result.
nextResult.set(eventWith("plugin.first"))
service.recalculateUpdates()
advanceUntilIdle()
withTimeout(1.seconds) {
service.flow().filterNotNull().first { it.hasUpdateFor("plugin.first") }
}
// A second recalculateUpdates() re-runs the provider check and picks up the new result.
nextResult.set(eventWith("plugin.second"))
service.recalculateUpdates()
advanceUntilIdle()
val second = withTimeout(1.seconds) {
service.flow().filterNotNull().first { it.hasUpdateFor("plugin.second") }
}
assertThat(second.all.map { it.pluginId.idString }).containsExactly("plugin.second")
}
@Test
fun `incremental updates are delivered as each provider emits`(@TestDisposable disposable: Disposable): Unit =
runTest {
val providerA = MutableSharedFlow<PluginUpdatesEvent>(replay = 1)
val providerB = MutableSharedFlow<PluginUpdatesEvent>(replay = 1)
ExtensionTestUtil.maskExtensions(
PluginUpdatesProvider.EP_NAME, listOf(FakeProvider(providerA), FakeProvider(providerB)), disposable,
)
val service = PluginUpdatesService(backgroundScope)
// Provider A emits first; its update must reach the merged flow even though provider B has not emitted yet.
providerA.emit(eventWith("plugin.a"))
advanceUntilIdle()
val afterA = withTimeout(1.seconds) {
service.flow().filterNotNull().first { it.hasUpdateFor("plugin.a") }
}
assertThat(afterA.hasUpdateFor("plugin.b"))
.describedAs("provider B's update must not be present before it emits")
.isFalse()
// Provider B emits; the merged result must now contain both providers' plugins.
providerB.emit(eventWith("plugin.b"))
advanceUntilIdle()
withTimeout(1.seconds) {
service.flow().filterNotNull().first { it.hasUpdateFor("plugin.a") && it.hasUpdateFor("plugin.b") }
}
}
private class FakeProvider(private val events: Flow<PluginUpdatesEvent?>) : PluginUpdatesProvider {
override suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?> = events
override suspend fun update() {}
}
private fun emptyEvent(): PluginUpdatesEvent = PluginUpdatesEvent(emptyList(), emptyList(), emptyList())
private fun eventWith(vararg pluginIds: String): PluginUpdatesEvent =
PluginUpdatesEvent(pluginIds.map { pluginDto(it, PluginSource.LOCAL) }, emptyList(), emptyList())
private fun PluginUpdatesEvent.hasUpdateFor(pluginId: String): Boolean =
all.any { it.pluginId.idString == pluginId }
private fun Collection<PluginUpdatesEvent>.hasUpdateFor(pluginId: String): Boolean =
any { it.hasUpdateFor(pluginId) }
}
@@ -3,19 +3,20 @@ package com.intellij.openapi.updateSettings.impl
import com.intellij.ide.plugins.api.PluginDto
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.util.BuildNumber
import org.jetbrains.annotations.ApiStatus
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import javax.swing.JComponent
@ApiStatus.Internal
class DefaultPluginUpdateHandler : PluginUpdateHandler {
private val downloaders = ConcurrentHashMap<String, PluginDownloaders>()
private val myDownloaders = ConcurrentHashMap<PluginId, PluginDownloader>()
override suspend fun loadAndStorePluginUpdates(
buildNumber: String?,
sessionId: String,
indicator: ProgressIndicator?,
): PluginUpdatesModel {
val buildNumber = BuildNumber.fromString(buildNumber)
@@ -26,48 +27,28 @@ class DefaultPluginUpdateHandler : PluginUpdateHandler {
val updateModels = notIgnoredDownloaders.map { it.uiModel }
val disabledUpdateModels = pluginUpdates.allDisabled.map { it.uiModel }
val incompatiblePluginNames = pluginUpdates.incompatible.map { it.name }
registerDownloaders(sessionId, notIgnoredDownloaders)
storeDownloaders(notIgnoredDownloaders)
val errors = internalPluginUpdates.errors.map { it.key to it.value.message.orEmpty() }.toMap()
val updateModel = PluginUpdatesModel(pluginUpdates = updateModels.map { PluginDto.fromModel(it) },
disabledPluginUpdates = disabledUpdateModels.map { PluginDto.fromModel(it) },
incompatiblePluginNames = incompatiblePluginNames,
updatesFromCustomRepositories = internalPluginUpdates.pluginNods.map { PluginDto.fromModel(it) },
internalErrors = errors,
sessionId = sessionId)
internalErrors = errors)
updateModel.downloaders = notIgnoredDownloaders
return updateModel
}
override suspend fun installUpdates(sessionId: String, updates: List<PluginUiModel>, component: JComponent?, finishCallback: Runnable?) {
val downloaders = updates.mapNotNull { getDownloader(sessionId, it.pluginId.idString) }
val callbackWrapper = {
finishCallback?.run()
deleteSession(sessionId)
}
PluginUpdateDialog.runUpdateAll(downloaders, component, callbackWrapper, null)
override suspend fun installUpdates(updates: Collection<PluginUiModel>, component: JComponent?, finishCallback: Runnable?, customRestarter: Consumer<Boolean>?) {
val downloaders = updates.mapNotNull { this.myDownloaders[it.pluginId] }
PluginUpdateDialog.runUpdateAll(downloaders, component, finishCallback, customRestarter)
}
override suspend fun ignorePluginUpdates(sessionId: String) {
UpdateCheckerFacade.getInstance().ignorePlugins(getDownloaders(sessionId).map { it.descriptor })
override suspend fun ignorePluginUpdates() {
UpdateCheckerFacade.getInstance().ignorePlugins(myDownloaders.values.map { it.descriptor })
}
private fun registerDownloader(sessionId: String, pluginId: String, downloader: PluginDownloader) {
downloaders.getOrPut(sessionId) { ConcurrentHashMap<String, PluginDownloader>() }[pluginId] = downloader
}
private fun registerDownloaders(sessionId: String, downloaders: List<PluginDownloader>) {
downloaders.forEach { registerDownloader(sessionId, it.descriptor.pluginId.idString, it) }
}
private fun getDownloader(sessionId: String, pluginId: String): PluginDownloader? {
return downloaders[sessionId]?.get(pluginId)
}
private fun getDownloaders(sessionId: String): List<PluginDownloader> = downloaders[sessionId]?.values?.toList() ?: emptyList()
private fun deleteSession(sessionId: String) {
downloaders.remove(sessionId)
private fun storeDownloaders(downloaders: List<PluginDownloader>) {
myDownloaders.clear()
downloaders.forEach { myDownloaders[it.descriptor.pluginId] = it }
}
}
typealias PluginDownloaders = ConcurrentHashMap<String, PluginDownloader>
@@ -267,8 +267,8 @@ public final class PlatformUpdateDialog extends AbstractUpdateDialog {
@Override
public void actionPerformed(ActionEvent e) {
close(OK_EXIT_CODE);
var downloaders = myUpdatesForPlugins != null ? myUpdatesForPlugins : Set.<PluginDownloader>of();
PluginModelAsyncOperationsExecutor.INSTANCE.findPlugins(downloaders, plugins -> {
var pluginIdsToUpdate = ContainerUtil.map2Set((myUpdatesForPlugins != null ? myUpdatesForPlugins : List.of()), PluginDownloader::getId);
PluginModelAsyncOperationsExecutor.INSTANCE.findPlugins(pluginIdsToUpdate, plugins -> {
downloadPatchAndRestart(plugins);
return Unit.INSTANCE;
});
@@ -305,11 +305,12 @@ public final class PlatformUpdateDialog extends AbstractUpdateDialog {
private void downloadPatchAndRestart(Map<PluginId, PluginUiModel> installedPlugins) {
Collection<PluginDownloader> selectedPluginsToUpdate = new ArrayList<>();
if (myUpdatesForPlugins != null && !installedPlugins.isEmpty()) {
var dialog = new PluginUpdateDialog(myProject, ContainerUtil.map(myUpdatesForPlugins, it -> it.getUiModel()), null, installedPlugins);
var dialog = new PluginUpdateDialog(myProject, new ArrayList<>(ContainerUtil.map(myUpdatesForPlugins, PluginDownloader::getUiModel)), null, installedPlugins);
if (!dialog.showAndGet()) {
return; // update cancelled
}
selectedPluginsToUpdate.addAll(PluginUpdateDialog.getSelectedDownloaders(myUpdatesForPlugins, dialog));
Set<PluginId> selectedPlugins = ContainerUtil.map2Set(dialog.getSelectedPluginModels(), PluginUiModel::getPluginId);
selectedPluginsToUpdate.addAll(ContainerUtil.filter(myUpdatesForPlugins, it -> selectedPlugins.contains(it.getId())));
}
//noinspection UsagesOfObsoleteApi
@@ -1,11 +1,18 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.updateSettings.impl
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.openapi.components.service
import com.intellij.platform.ide.CoreUiCoroutineScopeHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.function.Consumer
import javax.swing.JComponent
fun getPendingUpdates(): Collection<PluginDownloader>? = UpdateSettingsEntryPointActionProvider.getPendingUpdates()
fun getPendingUpdates(): Collection<PluginUiModel>? = UpdateSettingsEntryPointActionProvider.getPendingUpdates()
fun installUpdates(updates: Collection<PluginDownloader>, component: JComponent?, customRestarter: Consumer<Boolean>?) {
PluginUpdateDialog.runUpdateAll(updates, component, null, customRestarter)
fun installUpdates(updates: Collection<PluginUiModel>, component: JComponent?, customRestarter: Consumer<Boolean>?) {
service<CoreUiCoroutineScopeHolder>().coroutineScope.launch(Dispatchers.IO) {
PluginUpdateHandler.getInstance().installUpdates(updates, component, null, customRestarter)
}
}
@@ -789,10 +789,10 @@ private suspend fun doUpdateAndShowResult(
if (!showResults) {
if (platformUpdates is PlatformUpdates.Loaded) {
UpdateSettingsEntryPointActionProvider.newPlatformUpdate(platformUpdates, notIgnoredDownloaders, incompatiblePluginNames)
UpdateSettingsEntryPointActionProvider.newPlatformUpdate(platformUpdates, updatesForPlugins, incompatiblePluginNames, notIgnoredDownloaders)
}
else {
UpdateSettingsEntryPointActionProvider.newPluginUpdates(notIgnoredDownloaders, customRepoPlugins)
UpdateSettingsEntryPointActionProvider.newPluginUpdates(updatesForPlugins, customRepoPlugins)
}
callback?.setDone()
return null
@@ -814,7 +814,6 @@ private suspend fun doUpdateAndShowResult(
else {
showResults(
project = project,
sessionId = updatesModel.sessionId,
downloaders = notIgnoredDownloaders,
pluginUpdates = updatesForPlugins,
customRepoPlugins = customRepoPlugins,
@@ -840,7 +839,6 @@ private fun showErrors(project: Project?, @NlsContexts.DialogMessage message: St
@RequiresEdt
private suspend fun showResults(
project: Project?,
sessionId: String,
downloaders: List<PluginDownloader>,
pluginUpdates: List<PluginUiModel>,
customRepoPlugins: Collection<PluginUiModel>,
@@ -863,7 +861,7 @@ private suspend fun showResults(
if (dialog.showAndGet()) {
val selectedPlugins = dialog.getSelectedPluginModels()
service<CoreUiCoroutineScopeHolder>().coroutineScope.launch(Dispatchers.IO) {
PluginUpdateHandler.getInstance().installUpdates(sessionId, selectedPlugins, dialog.contentPanel, dialog.finishCallback)
PluginUpdateHandler.getInstance().installUpdates(selectedPlugins, dialog.contentPanel, dialog.finishCallback)
}
}
}
@@ -872,11 +870,11 @@ private suspend fun showResults(
showUpdateDialog()
}
else {
UpdateSettingsEntryPointActionProvider.newPluginUpdates(downloaders, customRepoPlugins)
UpdateSettingsEntryPointActionProvider.newPluginUpdates(pluginUpdates, customRepoPlugins)
if (userInitiated) {
// offer to update only enabled plugins
showUpdatePluginsNotification(sessionId, pluginUpdates, project, showUpdateDialog)
showUpdatePluginsNotification(pluginUpdates, project, showUpdateDialog)
}
}
}
@@ -917,7 +915,6 @@ private suspend fun showResults(
}
private fun showUpdatePluginsNotification(
sessionId: String,
updatesForPlugins: List<PluginUiModel>,
project: Project?,
showUpdateDialog: () -> Unit,
@@ -941,13 +938,13 @@ private fun showUpdatePluginsNotification(
updatesForPlugins.forEach {
PluginUpdateSourceService.getInstance().setPluginUpdateSourceId(it)
}
PluginUpdateHandler.getInstance().installUpdates(sessionId, updatesForPlugins, component, null)
PluginUpdateHandler.getInstance().installUpdates(updatesForPlugins, component, null)
}
},
NotificationAction.createSimpleExpiring(IdeBundle.message("updates.plugins.dialog.action"), showUpdateDialog),
NotificationAction.createSimpleExpiring(IdeBundle.message("updates.ignore.updates.link", updatesForPlugins.size)) {
coroutineScope.launch {
PluginUpdateHandler.getInstance().ignorePluginUpdates(sessionId)
PluginUpdateHandler.getInstance().ignorePluginUpdates()
}
},
),
@@ -982,7 +979,7 @@ private fun showResults(
showUpdateDialog()
}
else {
UpdateSettingsEntryPointActionProvider.newPlatformUpdate(platformUpdates, updatesForPlugins, incompatiblePluginNames)
UpdateSettingsEntryPointActionProvider.newPlatformUpdate(platformUpdates, updatesForPlugins.map { it.uiModel }, incompatiblePluginNames, updatesForPlugins)
if (showNotification) {
IdeUpdateUsageTriggerCollector.NOTIFICATION_SHOWN.log(project)
@@ -4,18 +4,16 @@ package com.intellij.openapi.updateSettings.impl;
import com.intellij.ide.AppLifecycleListener;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginStateListener;
import com.intellij.ide.plugins.PluginStateManager;
import com.intellij.ide.plugins.newui.PluginUpdatesService;
import com.intellij.ide.plugins.newui.PluginModelAsyncOperationsExecutor;
import com.intellij.ide.plugins.newui.PluginUiModel;
import com.intellij.ide.plugins.newui.PluginUpdatesService;
import com.intellij.ide.plugins.newui.PluginUpdateSubscription;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
@@ -24,6 +22,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.BuildNumber;
import com.intellij.openapi.util.Pair;
import com.intellij.platform.ide.productMode.IdeProductMode;
import com.intellij.util.containers.ContainerUtil;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
@@ -51,11 +50,11 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
private static @Nullable List<String> myIncompatiblePluginNames;
private static @Nullable Set<String> myAlreadyShownPluginUpdates;
private static @Nullable Collection<PluginDownloader> myUpdatesForPlugins;
private static @Nullable Collection<PluginUiModel> myUpdatesForPlugins;
private static @Nullable Collection<PluginUiModel> myCustomRepositoryPlugins;
private static @Nullable Collection<PluginDownloader> myLocalUpdatesForPlugins;
private static PluginUpdatesService myUpdatesService;
private static PluginStateListener myPluginStateListener;
private static PluginUpdateSubscription myPluginUpdatesSubscription;
private static boolean myEnableUpdateAction = true;
@@ -108,9 +107,9 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
}
private static void initPluginsListeners() {
if (myUpdatesService == null) {
myUpdatesService = PluginUpdatesService.connectWithUpdates(updateResult -> {
if (updateResult.isEmpty()) {
if (myPluginUpdatesSubscription == null) {
myPluginUpdatesSubscription = PluginUpdatesService.getInstance().subscribe(updateResult -> {
if (updateResult.getEnabledUpdates().isEmpty()) {
newUpdatesForPlugins(null);
myCustomRepositoryPlugins = null;
return;
@@ -118,34 +117,22 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
if (!UpdateSettings.getInstance().isPluginsCheckNeeded()) {
return;
}
newUpdatesForPlugins(updateResult.getPluginUpdates().getAll());
myCustomRepositoryPlugins = updateResult.getPluginNods();
});
}
if (myPluginStateListener == null) {
PluginStateManager.addStateListener(myPluginStateListener = new PluginStateListener() {
@Override
public void install(@NotNull IdeaPluginDescriptor descriptor) {
removePluginsUpdate(List.of(descriptor));
}
@Override
public void uninstall(@NotNull IdeaPluginDescriptor descriptor) {
install(descriptor);
}
newUpdatesForPlugins(new ArrayList<>(updateResult.getEnabledUpdates()));
myCustomRepositoryPlugins = new ArrayList<>(updateResult.getPluginNods());
});
}
}
private static void clearUpdatesInfo() {
setPlatformUpdateInfo(null);
newPlatformUpdate(null, null, (String)null);
newPlatformUpdate(null, null, (String)null, null);
updateState();
}
public static void newPlatformUpdate(@NotNull PlatformUpdates.Loaded platformUpdateInfo,
@NotNull List<PluginDownloader> updatesForPlugins,
@NotNull List<String> incompatiblePluginNames) {
@NotNull List<PluginUiModel> updatesForPlugins,
@NotNull List<String> incompatiblePluginNames,
@NotNull List<PluginDownloader> localUpdatesForPlugins) {
UpdateSettings settings = UpdateSettings.getInstance();
if (settings.isCheckNeeded()) {
setPlatformUpdateInfo(platformUpdateInfo);
@@ -154,10 +141,10 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
setPlatformUpdateInfo(null);
}
if (settings.isPluginsCheckNeeded()) {
newPlatformUpdate(updatesForPlugins, incompatiblePluginNames, null);
newPlatformUpdate(updatesForPlugins, incompatiblePluginNames, null, localUpdatesForPlugins);
}
else {
newPlatformUpdate(null, null, (String)null);
newPlatformUpdate(null, null, (String)null, null);
}
updateState();
}
@@ -180,15 +167,17 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
}
}
private static void newPlatformUpdate(@Nullable List<PluginDownloader> updatesForPlugins,
private static void newPlatformUpdate(@Nullable List<PluginUiModel> updatesForPlugins,
@Nullable List<String> incompatiblePluginNames,
@Nullable String nextRunPlatformUpdateVersion) {
@Nullable String nextRunPlatformUpdateVersion,
@Nullable List<PluginDownloader> localUpdatesForPlugins) {
myUpdatesForPlugins = updatesForPlugins;
myIncompatiblePluginNames = incompatiblePluginNames;
myNextRunPlatformUpdateVersion = nextRunPlatformUpdateVersion;
myLocalUpdatesForPlugins = localUpdatesForPlugins;
}
public static void newPluginUpdates(@NotNull Collection<PluginDownloader> updatesForPlugins,
public static void newPluginUpdates(@NotNull Collection<PluginUiModel> updatesForPlugins,
@NotNull Collection<PluginUiModel> customRepositoryPlugins) {
if (UpdateSettings.getInstance().isPluginsCheckNeeded()) {
myUpdatesForPlugins = updatesForPlugins;
@@ -201,24 +190,24 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
updateState();
}
public static @Nullable Collection<PluginDownloader> getPendingUpdates() {
public static @Nullable Collection<PluginUiModel> getPendingUpdates() {
return myUpdatesForPlugins;
}
private static void newUpdatesForPlugins(@Nullable Collection<PluginDownloader> updatesForPlugins) {
private static void newUpdatesForPlugins(@Nullable Collection<PluginUiModel> updatesForPlugins) {
myUpdatesForPlugins = ContainerUtil.isEmpty(updatesForPlugins) ? null : updatesForPlugins;
updateState();
}
static void removePluginsUpdate(@NotNull List<? extends IdeaPluginDescriptor> descriptors) {
if (myAlreadyShownPluginUpdates != null) {
myAlreadyShownPluginUpdates.removeIf(name -> descriptors.stream().anyMatch(descriptor -> name.equals(descriptor.getName())));
myAlreadyShownPluginUpdates.removeIf(name -> ContainerUtil.exists(descriptors, descriptor -> name.equals(descriptor.getName())));
}
if (myUpdatesForPlugins != null) {
Set<PluginId> pluginIds = ContainerUtil.map2Set(descriptors,
IdeaPluginDescriptor::getPluginId);
List<PluginDownloader> updatesForPlugins = ContainerUtil.filter(myUpdatesForPlugins,
downloader -> !pluginIds.contains(downloader.getId()));
List<PluginUiModel> updatesForPlugins = ContainerUtil.filter(myUpdatesForPlugins,
downloader -> !pluginIds.contains(downloader.getPluginId()));
if (myUpdatesForPlugins.size() != updatesForPlugins.size()) {
newUpdatesForPlugins(updatesForPlugins);
}
@@ -227,7 +216,7 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
private static boolean isAlreadyShownPluginUpdates() {
return myUpdatesForPlugins == null || ContainerUtil.isEmpty(myAlreadyShownPluginUpdates) ||
myUpdatesForPlugins.stream().anyMatch(plugin -> !myAlreadyShownPluginUpdates.contains(plugin.getPluginName()));
ContainerUtil.exists(myUpdatesForPlugins, plugin -> !myAlreadyShownPluginUpdates.contains(plugin.getName()));
}
private static void updateAlreadyShownPluginUpdates() {
@@ -235,7 +224,7 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
if (myAlreadyShownPluginUpdates == null) {
myAlreadyShownPluginUpdates = new HashSet<>();
}
myUpdatesForPlugins.forEach(plugin -> myAlreadyShownPluginUpdates.add(plugin.getPluginName()));
myUpdatesForPlugins.forEach(plugin -> myAlreadyShownPluginUpdates.add(plugin.getName()));
}
}
@@ -252,35 +241,35 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
Pair<PlatformUpdates, InternalPluginResults> result = ProgressManager.getInstance()
Pair<PlatformUpdates, PluginUpdatesModel> result = ProgressManager.getInstance()
.run(new Task.WithResult<>(project,
IdeBundle.message("find.ide.update.title"),
true) {
@Override
protected @NotNull Pair<@NotNull PlatformUpdates, @Nullable InternalPluginResults> compute(@NotNull ProgressIndicator indicator) {
protected @NotNull Pair<@NotNull PlatformUpdates, @Nullable PluginUpdatesModel> compute(@NotNull ProgressIndicator indicator) {
PlatformUpdates platformUpdates = UpdateChecker.getPlatformUpdates(UpdateSettings.getInstance(), indicator);
InternalPluginResults pluginResults = platformUpdates instanceof PlatformUpdates.Loaded ?
PluginUpdatesModel pluginResults = platformUpdates instanceof PlatformUpdates.Loaded ?
getInternalPluginUpdates((PlatformUpdates.Loaded)platformUpdates, indicator) :
null;
return Pair.create(platformUpdates, pluginResults);
}
private static @NotNull InternalPluginResults getInternalPluginUpdates(@NotNull PlatformUpdates.Loaded loadedResult,
private static PluginUpdatesModel getInternalPluginUpdates(@NotNull PlatformUpdates.Loaded loadedResult,
@NotNull ProgressIndicator indicator) {
return UpdateCheckerFacade.getInstance().checkInstalledPluginUpdates(
indicator, loadedResult.getNewBuild().getApiVersion()
);
return PluginUpdateHandler.loadAndStorePluginUpdates(loadedResult.getNewBuild().getApiVersion().asString(),
indicator);
}
});
PlatformUpdates platformUpdateInfo = result.getFirst();
InternalPluginResults pluginResults = result.getSecond();
if (platformUpdateInfo instanceof PlatformUpdates.Loaded && pluginResults != null) {
PluginUpdatesModel pluginUpdatesModel = result.getSecond();
if (platformUpdateInfo instanceof PlatformUpdates.Loaded && pluginUpdatesModel != null) {
setPlatformUpdateInfo((PlatformUpdates.Loaded)platformUpdateInfo);
newPlatformUpdate(pluginResults.getPluginUpdates().getAllEnabled().stream().toList(),
ContainerUtil.map(pluginResults.getPluginUpdates().getIncompatible(), PluginDescriptor::getName),
null);
newPlatformUpdate(new ArrayList<>(pluginUpdatesModel.getPluginUpdates()),
pluginUpdatesModel.getIncompatiblePluginNames(),
null,
pluginUpdatesModel.getDownloaders());
super.actionPerformed(e);
}
else {
@@ -304,12 +293,12 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
actions.add(new IdeUpdateAction(myPlatformUpdateInfo.getNewBuild().getVersion()));
}
// todo[AL/RS] separate action for plugins compatible with both old and new builds
else if (myUpdatesForPlugins != null && !myUpdatesForPlugins.isEmpty()) {
else if (myUpdatesForPlugins != null && !myUpdatesForPlugins.isEmpty() && !IdeProductMode.isBackend()) {
int size = myUpdatesForPlugins.size();
actions.add(new UpdateAction(size == 1
? IdeBundle.message("settings.entry.point.update.plugin.action",
myUpdatesForPlugins.iterator().next().getPluginName())
myUpdatesForPlugins.iterator().next().getName())
: IdeBundle.message("settings.entry.point.update.plugins.action", size)) {
@Override
public boolean isPluginUpdate() {
@@ -338,13 +327,11 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Collection<PluginDownloader> updatesForPlugins = myUpdatesForPlugins;
PluginModelAsyncOperationsExecutor.INSTANCE.findPlugins(updatesForPlugins, plugins -> {
List<@Nullable PluginUiModel> updateModels = ContainerUtil.map(updatesForPlugins, it -> it.getUiModel());
var dialog = new PluginUpdateDialog(e.getProject(), updateModels, myCustomRepositoryPlugins, plugins);
PluginModelAsyncOperationsExecutor.INSTANCE.findPlugins(ContainerUtil.map(myUpdatesForPlugins, PluginUiModel::getPluginId), installedPlugins -> {
var dialog = new PluginUpdateDialog(e.getProject(), myUpdatesForPlugins, myCustomRepositoryPlugins, installedPlugins);
dialog.setFinishCallback(() -> setEnableUpdateAction(true));
setEnableUpdateAction(false);
if (!PluginUpdateDialog.showDialogAndUpdate(updatesForPlugins, dialog)) {
if (!PluginUpdateDialog.showDialogAndUpdate(dialog)) {
setEnableUpdateAction(true);
}
return Unit.INSTANCE;
@@ -380,7 +367,7 @@ final class UpdateSettingsEntryPointActionProvider implements ActionProvider {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
PlatformUpdateDialog dialog = new PlatformUpdateDialog(e.getProject(), Objects.requireNonNull(myPlatformUpdateInfo),
true, myUpdatesForPlugins, myIncompatiblePluginNames);
true, myLocalUpdatesForPlugins, myIncompatiblePluginNames);
if (dialog.showAndGet()) {
clearUpdatesInfo();
}
@@ -716,6 +716,9 @@
<extensionPoint name="uiPluginManagerController"
interface="com.intellij.ide.plugins.newui.UiPluginManagerController"
dynamic="true"/>
<extensionPoint name="pluginUpdatesProvider"
interface="com.intellij.ide.plugins.newui.PluginUpdatesProvider"
dynamic="true"/>
<extensionPoint name="pluginManagerCustomizer"
interface="com.intellij.ide.plugins.newui.PluginManagerCustomizer"
dynamic="true"/>
@@ -15,8 +15,6 @@ import com.intellij.ide.plugins.marketplace.PluginSearchResult
import com.intellij.ide.plugins.marketplace.SetEnabledStateResult
import com.intellij.ide.plugins.newui.DefaultUiPluginManagerController
import com.intellij.ide.plugins.newui.PluginInstallationState
import com.intellij.ide.plugins.newui.PluginManagerSessionService
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.openapi.application.EDT
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.updateSettings.impl.UpdateSettings
@@ -24,9 +22,6 @@ import com.intellij.platform.pluginManager.shared.rpc.PluginManagerApi
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProjectOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@@ -52,10 +47,6 @@ class BackendPluginManagerApi : PluginManagerApi {
return DefaultUiPluginManagerController.getInstalledPlugins().map { PluginDto.fromModel(it, true) }
}
override suspend fun getUpdates(): List<PluginDto> {
return DefaultUiPluginManagerController.getUpdates().map { PluginDto.fromModel(it) }
}
override suspend fun setEnabledState(sessionId: String, pluginIds: List<PluginId>, enable: Boolean) {
DefaultUiPluginManagerController.setPluginStatus(sessionId, pluginIds, enable)
}
@@ -214,32 +205,6 @@ class BackendPluginManagerApi : PluginManagerApi {
return DefaultUiPluginManagerController.getLastCompatiblePluginUpdate(allIds, throwExceptions, buildNumber)
}
override suspend fun isNeedUpdate(pluginId: PluginId): Boolean {
return DefaultUiPluginManagerController.isNeedUpdate(pluginId)
}
override suspend fun subscribeToPluginUpdates(sessionId: String): Flow<List<PluginDto>> {
return channelFlow {
DefaultUiPluginManagerController.connectToPluginUpdateService(sessionId) { pluginUiModels: List<PluginUiModel>? ->
trySend(pluginUiModels?.map { PluginDto.fromModel(it) } ?: emptyList())
}
awaitClose()
}
}
override suspend fun recalculatePluginUpdates(sessionId: String) {
PluginManagerSessionService.getInstance().getSession(sessionId)?.updateService?.recalculateUpdates()
}
override suspend fun disposeUpdaterService(sessionId: String) {
PluginManagerSessionService.getInstance().getSession(sessionId)?.updateService?.dispose()
}
override suspend fun notifyUpdateFinished(sessionId: String) {
PluginManagerSessionService.getInstance().getSession(sessionId)?.updateService?.finishUpdate()
}
override suspend fun checkPluginCanBeDownloaded(plugin: PluginDto): Boolean {
return DefaultUiPluginManagerController.checkPluginCanBeDownloaded(plugin, null)
}
@@ -16,8 +16,8 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class BackendPluginUpdaterApi : PluginUpdaterApi {
override suspend fun loadAndStorePluginUpdates(apiVersion: String?, sessionId: String): PluginUpdatesModel {
val updates = PluginUpdateHandler.getInstance().loadAndStorePluginUpdates(apiVersion, sessionId)
override suspend fun loadAndStorePluginUpdates(apiVersion: String?): PluginUpdatesModel {
val updates = PluginUpdateHandler.getInstance().loadAndStorePluginUpdates(apiVersion)
val pluginAutoUpdateService = service<PluginAutoUpdateService>()
if (pluginAutoUpdateService.isAutoUpdateEnabled()) {
pluginAutoUpdateService.onPluginUpdatesChecked(updates.downloaders)
@@ -25,10 +25,10 @@ class BackendPluginUpdaterApi : PluginUpdaterApi {
return updates
}
override suspend fun installUpdates(sessionId: String, updates: List<PluginDto>): Deferred<Boolean> {
override suspend fun installUpdates(updates: List<PluginDto>): Deferred<Boolean> {
return serviceAsync<PluginManagerCoroutineScopeHolder>().cs.async {
try {
PluginUpdateHandler.getInstance().installUpdates(sessionId, updates, null, null)
PluginUpdateHandler.getInstance().installUpdates(updates, null, null)
}
catch (_: Exception) {
return@async false
@@ -37,8 +37,8 @@ class BackendPluginUpdaterApi : PluginUpdaterApi {
}
}
override suspend fun ignorePluginUpdates(sessionId: String) {
PluginUpdateHandler.getInstance().ignorePluginUpdates(sessionId)
override suspend fun ignorePluginUpdates() {
PluginUpdateHandler.getInstance().ignorePluginUpdates()
}
}
@@ -0,0 +1,19 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.pluginManager.backend.rpc
import com.intellij.ide.plugins.newui.PluginUpdatesEvent
import com.intellij.ide.plugins.newui.PluginUpdatesProvider
import com.intellij.platform.pluginManager.shared.rpc.PluginUpdatesProviderApi
import kotlinx.coroutines.flow.Flow
internal class BackendPluginUpdatesProviderApi() : PluginUpdatesProviderApi {
private val delegate = PluginUpdatesProvider.getInstances().first()
override suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?> {
return delegate.pluginUpdateEvents()
}
override suspend fun update() {
delegate.update()
}
}
@@ -5,11 +5,10 @@ import com.intellij.platform.pluginManager.shared.rpc.PluginErrorReporterApi
import com.intellij.platform.pluginManager.shared.rpc.PluginInstallerApi
import com.intellij.platform.pluginManager.shared.rpc.PluginManagerApi
import com.intellij.platform.pluginManager.shared.rpc.PluginUpdaterApi
import com.intellij.platform.pluginManager.shared.rpc.PluginUpdatesProviderApi
import com.intellij.platform.rpc.backend.RemoteApiProvider
import fleet.rpc.remoteApiDescriptor
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal class PluginManagerApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<PluginManagerApi>()) {
@@ -27,5 +26,9 @@ internal class PluginManagerApiProvider : RemoteApiProvider {
remoteApi(remoteApiDescriptor<PluginErrorReporterApi>()) {
BackendPluginErrorReporterApi()
}
remoteApi(remoteApiDescriptor<PluginUpdatesProviderApi>()) {
BackendPluginUpdatesProviderApi()
}
}
}
@@ -16,7 +16,6 @@ import com.intellij.ide.plugins.marketplace.SetEnabledStateResult
import com.intellij.ide.plugins.newui.PluginInstallationState
import com.intellij.ide.plugins.newui.PluginSource
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.ide.plugins.newui.UiPluginManagerController
import com.intellij.ide.ui.search.TraverseUIMode
import com.intellij.openapi.application.ModalityState
@@ -29,13 +28,9 @@ import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.FUSEventSou
import com.intellij.platform.pluginManager.shared.rpc.PluginInstallerApi
import com.intellij.platform.pluginManager.shared.rpc.PluginManagerApi
import com.intellij.platform.project.projectId
import fleet.rpc.client.durable
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.ApiStatus
@@ -65,10 +60,6 @@ class BackendUiPluginManagerController() : UiPluginManagerController {
return PluginManagerApi.getInstance().getInstalledPlugins().withSource()
}
override suspend fun getUpdates(): List<PluginUiModel> {
return PluginManagerApi.getInstance().getUpdates().withSource()
}
override suspend fun getPlugin(id: PluginId): PluginUiModel? {
return PluginManagerApi.getInstance().getPluginById(id)?.withSource()
}
@@ -176,19 +167,6 @@ class BackendUiPluginManagerController() : UiPluginManagerController {
return PluginManagerApi.getInstance().loadErrors(sessionId, pluginIds)
}
@OptIn(FlowPreview::class)
override fun connectToPluginUpdateService(sessionId: String, callback: (List<PluginUiModel>) -> Unit): PluginUpdatesService {
val result = RemotePluginUpdatesService(sessionId)
result.coroutineScope.launch {
durable {
PluginManagerApi.getInstance().subscribeToPluginUpdates(sessionId).debounce(100).collectLatest {
callback(it)
}
}
}
return result
}
override fun filterPluginsRequiringUltimateButItsDisabled(pluginIds: List<PluginId>): List<PluginId> {
return awaitForResult { PluginManagerApi.getInstance().filterPluginsRequiresUltimateButItsDisabled(pluginIds) }
}
@@ -271,10 +249,6 @@ class BackendUiPluginManagerController() : UiPluginManagerController {
}
}
override suspend fun isNeedUpdate(pluginId: PluginId): Boolean {
return PluginManagerApi.getInstance().isNeedUpdate(pluginId)
}
override suspend fun closeSession(sessionId: String) {
service<BackendRpcCoroutineContext>().coroutineScope.launch {
PluginManagerApi.getInstance().closeSession(sessionId)
@@ -1,48 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.pluginManager.frontend
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.plugins.newui.PluginUpdatesService
import com.intellij.openapi.components.service
import com.intellij.platform.pluginManager.shared.rpc.PluginManagerApi
import com.intellij.platform.util.coroutines.childScope
import fleet.rpc.client.durable
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import java.util.function.Consumer
@ApiStatus.Internal
open class RemotePluginUpdatesService(private val sessionId: String) : PluginUpdatesService() {
internal val coroutineScope = service<BackendRpcCoroutineContext>().coroutineScope.childScope("RemotePluginUpdatesServiceScope")
override fun calculateUpdates(callback: Consumer<in Collection<PluginUiModel>>) {
coroutineScope.launch {
durable {
PluginManagerApi.getInstance().subscribeToPluginUpdates(sessionId).collect {
callback.accept(it)
}
}
}
}
override fun recalculateUpdates() {
coroutineScope.launch {
PluginManagerApi.getInstance().recalculatePluginUpdates(sessionId)
}
}
override fun dispose() {
coroutineScope.launch {
PluginManagerApi.getInstance().disposeUpdaterService(sessionId)
coroutineScope.cancel()
}
}
override fun finishUpdate() {
coroutineScope.launch {
PluginManagerApi.getInstance().notifyUpdateFinished(sessionId)
}
}
}
@@ -17,7 +17,6 @@ import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.flow.Flow
import org.jetbrains.annotations.ApiStatus
@Rpc
@@ -27,7 +26,6 @@ interface PluginManagerApi : RemoteApi<Unit> {
suspend fun getPluginById(pluginId: PluginId): PluginDto?
suspend fun getVisiblePlugins(showImplementationDetails: Boolean): List<PluginDto>
suspend fun getInstalledPlugins(): List<PluginDto>
suspend fun getUpdates(): List<PluginDto>
suspend fun findPlugin(pluginId: PluginId): PluginDto?
suspend fun getLastCompatiblePluginUpdateModel(pluginId: PluginId, buildNumber: String?): PluginDto?
suspend fun getLastCompatiblePluginUpdate(allIds: Set<PluginId>, throwExceptions: Boolean, buildNumber: String?): List<IdeCompatibleUpdate>
@@ -54,11 +52,6 @@ interface PluginManagerApi : RemoteApi<Unit> {
suspend fun getPluginsRequiresUltimateMap(pluginIds: List<PluginId>): Map<PluginId, Boolean>
suspend fun filterPluginsRequiresUltimateButItsDisabled(pluginIds: List<PluginId>): List<PluginId>
suspend fun findPluginNames(pluginIds: List<PluginId>): List<String>
suspend fun isNeedUpdate(pluginId: PluginId): Boolean
suspend fun subscribeToPluginUpdates(sessionId: String): Flow<List<PluginDto>>
suspend fun recalculatePluginUpdates(sessionId: String)
suspend fun disposeUpdaterService(sessionId: String)
suspend fun notifyUpdateFinished(sessionId: String)
suspend fun getPluginInstallationState(pluginId: PluginId): PluginInstallationState
suspend fun getPluginInstallationStates(): Map<PluginId, PluginInstallationState>
suspend fun checkPluginCanBeDownloaded(plugin: PluginDto): Boolean
@@ -13,11 +13,11 @@ import org.jetbrains.annotations.ApiStatus
@Rpc
@ApiStatus.Internal
interface PluginUpdaterApi : RemoteApi<Unit> {
suspend fun loadAndStorePluginUpdates(apiVersion: String?, sessionId: String): PluginUpdatesModel
suspend fun loadAndStorePluginUpdates(apiVersion: String?): PluginUpdatesModel
suspend fun installUpdates(sessionId: String, updates: List<PluginDto>): Deferred<Boolean>
suspend fun installUpdates(updates: List<PluginDto>): Deferred<Boolean>
suspend fun ignorePluginUpdates(sessionId: String)
suspend fun ignorePluginUpdates()
companion object {
suspend fun getInstance(): PluginUpdaterApi {
@@ -0,0 +1,24 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.pluginManager.shared.rpc
import com.intellij.ide.plugins.newui.PluginUpdatesEvent
import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.flow.Flow
import org.jetbrains.annotations.ApiStatus
@Rpc
@Suppress("NonSerializableTypeInRpcInterface")
@ApiStatus.Internal
interface PluginUpdatesProviderApi : RemoteApi<Unit> {
suspend fun pluginUpdateEvents(): Flow<PluginUpdatesEvent?>
suspend fun update()
companion object {
suspend fun getInstance(): PluginUpdatesProviderApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<PluginUpdatesProviderApi>())
}
}
}
@@ -60,7 +60,7 @@ internal class PluginsUpdateStep: SmartUpdateStep {
val updates = getPendingUpdates()
if (updates.isNullOrEmpty()) return SmartUpdateBundle.message("no.updates.available")
return if (updates.size == 1)
SmartUpdateBundle.message("update.plugin", updates.first().pluginName) else
SmartUpdateBundle.message("update.plugin", updates.first().name ?: "") else
SmartUpdateBundle.message("update.several.plugins", updates.size)
}
}