[feature suggestion] don't leak notifications with the project

Disposing alarm doesn't cancel delay which is attached to the application level coroutine scope by default; thus it waits for 10 seconds and might prevent test for leaked project perform normally

^KTIJ-31624 fixed

GitOrigin-RevId: 8879b47391699175fb8440465444c959f87da077
This commit is contained in:
Anna Kozlova
2024-10-15 09:40:44 +00:00
committed by intellij-monorepo-bot
parent 6c2b4a6022
commit 93e897da83
2 changed files with 37 additions and 19 deletions
@@ -11,6 +11,7 @@ import com.intellij.openapi.editor.ex.EditorEventMulticasterEx
import com.intellij.openapi.editor.ex.FocusChangeListener
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import training.featuresSuggester.actions.Action
import training.featuresSuggester.actions.EditorFocusGainedAction
import training.featuresSuggester.settings.FeatureSuggesterSettings
@@ -19,7 +20,7 @@ import training.featuresSuggester.ui.NotificationSuggestionPresenter
import training.featuresSuggester.ui.SuggestionPresenter
@Service(Service.Level.PROJECT)
internal class FeatureSuggestersManager(private val project: Project) : Disposable {
internal class FeatureSuggestersManager(private val project: Project, private val coroutineScope: CoroutineScope) : Disposable {
private val suggestionPresenter: SuggestionPresenter = NotificationSuggestionPresenter()
init {
@@ -56,7 +57,7 @@ internal class FeatureSuggestersManager(private val project: Project) : Disposab
if (suggestion is PopupSuggestion) {
suggester.logStatisticsThatSuggestionIsFound(suggestion)
if (suggester.isEnabled() && (SuggestingUtils.forceShowSuggestions || suggester.isSuggestionNeeded())) {
suggestionPresenter.showSuggestion(project, suggestion, disposable = this)
suggestionPresenter.showSuggestion(project, suggestion, coroutineScope = coroutineScope)
fireSuggestionFound(suggestion)
FeatureSuggesterSettings.instance().updateSuggestionShownTime(suggestion.suggesterId)
}
@@ -8,16 +8,16 @@ import com.intellij.notification.Notification
import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.ComponentManagerEx
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.project.Project
import com.intellij.util.Alarm
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import training.featuresSuggester.DocumentationSuggestion
import training.featuresSuggester.FeatureSuggesterBundle
import training.featuresSuggester.PopupSuggestion
@@ -26,7 +26,7 @@ import training.featuresSuggester.settings.FeatureSuggesterSettings
import training.featuresSuggester.statistics.FeatureSuggesterStatistics
internal interface SuggestionPresenter {
fun showSuggestion(project: Project, suggestion: PopupSuggestion, disposable: Disposable)
fun showSuggestion(project: Project, suggestion: PopupSuggestion, coroutineScope: CoroutineScope)
}
@Suppress("DialogTitleCapitalization")
@@ -34,34 +34,42 @@ internal class NotificationSuggestionPresenter : SuggestionPresenter {
private val notificationGroup: NotificationGroup = NotificationGroupManager.getInstance()
.getNotificationGroup("IDE Feature Suggester")
override fun showSuggestion(project: Project, suggestion: PopupSuggestion, disposable: Disposable) {
override fun showSuggestion(project: Project, suggestion: PopupSuggestion, coroutineScope: CoroutineScope) {
val notification = notificationGroup.createNotification(
title = FeatureSuggesterBundle.message("notification.title"),
content = suggestion.message,
type = NotificationType.INFORMATION
)
val expireJob = coroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement(), start = CoroutineStart.LAZY) {
if (!notification.isExpired) {
delay(10_000)
notification.expire()
}
}
when (suggestion) {
is TipSuggestion -> {
val action = createShowTipAction(project, notification, suggestion)
val action = createShowTipAction(project, notification, suggestion, coroutineScope, expireJob)
if (action != null) {
notification.addAction(action)
}
}
is DocumentationSuggestion -> {
notification.addAction(createGoToDocumentationAction(notification, suggestion))
notification.addAction(createGoToDocumentationAction(notification, suggestion, expireJob))
}
}
notification.addAction(createDontSuggestAction(notification, suggestion))
notification.addAction(createDontSuggestAction(notification, suggestion, expireJob))
notification.notify(project)
Alarm(disposable).addRequest(notification::expire, 10000, ModalityState.any())
expireJob.start()
FeatureSuggesterStatistics.logNotificationShowed(suggestion.suggesterId)
}
private fun createDontSuggestAction(notification: Notification, suggestion: PopupSuggestion): AnAction {
private fun createDontSuggestAction(notification: Notification, suggestion: PopupSuggestion, expireJob: Job): AnAction {
return object : AnAction(FeatureSuggesterBundle.message("notification.dont.suggest")) {
override fun actionPerformed(e: AnActionEvent) {
expireJob.cancel()
val settings = FeatureSuggesterSettings.instance()
settings.setEnabled(suggesterId = suggestion.suggesterId, enabled = false)
notification.hideBalloon()
@@ -70,7 +78,7 @@ internal class NotificationSuggestionPresenter : SuggestionPresenter {
}
}
private fun createGoToDocumentationAction(notification: Notification, suggestion: DocumentationSuggestion): AnAction {
private fun createGoToDocumentationAction(notification: Notification, suggestion: DocumentationSuggestion, expireJob: Job): AnAction {
return object : AnAction(
FeatureSuggesterBundle.message(
"notification.open.help",
@@ -78,6 +86,7 @@ internal class NotificationSuggestionPresenter : SuggestionPresenter {
)
) {
override fun actionPerformed(e: AnActionEvent) {
expireJob.cancel()
BrowserUtil.open(suggestion.documentURL)
notification.hideBalloon()
FeatureSuggesterStatistics.logNotificationLearnMore(suggestion.suggesterId)
@@ -85,14 +94,22 @@ internal class NotificationSuggestionPresenter : SuggestionPresenter {
}
}
private fun createShowTipAction(project: Project, notification: Notification, suggestion: TipSuggestion): AnAction? {
private fun createShowTipAction(
project: Project,
notification: Notification,
suggestion: TipSuggestion,
coroutineScope: CoroutineScope,
expireJob: Job,
): AnAction? {
val tip = TipAndTrickBean.findById(suggestion.suggestingTipId) ?: return null
return object : AnAction(FeatureSuggesterBundle.message("notification.learn.more")) {
override fun actionPerformed(e: AnActionEvent) {
(project as ComponentManagerEx).getCoroutineScope().launch {
TipAndTrickManager.getInstance().showTipDialog(project, tip)
notification.hideBalloon()
FeatureSuggesterStatistics.logNotificationLearnMore(suggestion.suggesterId)
expireJob.cancel()
notification.hideBalloon()
FeatureSuggesterStatistics.logNotificationLearnMore(suggestion.suggesterId)
coroutineScope.launch {
serviceAsync<TipAndTrickManager>().showTipDialog(project, tip)
}
}