mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[remote development, platform] IJPL-245265 Refactor and split PluginUpdateService
GitOrigin-RevId: 8366ddc672069765b05bd32f0c262453a590a6f9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
75fc0d8144
commit
91a5d2a2aa
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+23
-16
@@ -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()
|
||||
|
||||
+9
-8
@@ -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();
|
||||
}
|
||||
|
||||
+111
@@ -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 }
|
||||
}
|
||||
-25
@@ -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 {
|
||||
|
||||
-1
@@ -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)
|
||||
|
||||
+3
-4
@@ -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 }
|
||||
}
|
||||
+170
-291
@@ -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>
|
||||
|
||||
-4
@@ -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)
|
||||
|
||||
+8
-24
@@ -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) {
|
||||
|
||||
+25
-5
@@ -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>,
|
||||
|
||||
+7
-6
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+261
@@ -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) }
|
||||
}
|
||||
+13
-32
@@ -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>
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
+10
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
+8
-11
@@ -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)
|
||||
|
||||
+47
-60
@@ -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"/>
|
||||
|
||||
-35
@@ -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)
|
||||
}
|
||||
|
||||
+6
-6
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -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
-2
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
-26
@@ -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)
|
||||
|
||||
-48
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-7
@@ -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
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user