From 9a36684baf443442af635dd3802deea38c2f74b8 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Mon, 2 Mar 2026 20:30:58 +0100 Subject: [PATCH] PY-88057 Replace "Configure Python Interpreter" with "Custom Environment" dropdown - Add a dropdown menu listing available interpreters (venv, conda, system, etc.) instead of a single "Configure Python Interpreter" link - Introduce BusyGuardExecutor interface and InterpreterFixExecutor service that shares busy state across all notification panels via MutableStateFlow; while a fix is running, panels show a spinner instead of action links - Move SDK creation work off EDT through the executor's coroutine scope - Refresh module content roots after SDK creation instead of full VFS refresh - Remove redundant SdkConfigurationService and InspectionRunnerService, consolidating their scopes into InterpreterFixExecutor (cherry picked from commit 513beeaf0565721540aec4e248ea00ba7948e0cf) GitOrigin-RevId: b1eed6952e43b229b01363a0ddac9b9269f241f3 --- .../messages/PyBundle.properties | 2 + .../PyAsyncFileInspectionRunner.kt | 47 ++++++-- .../InterpreterSettingsQuickFix.kt | 103 +++++++++++++++--- .../PyInterpreterNotificationProvider.kt | 26 +++-- .../PyProjectSdkConfiguration.kt | 40 +------ .../configuration/PyVenvSdkConfiguration.kt | 3 +- .../PyAsyncFileInspectionRunnerTest.kt | 29 +++-- 7 files changed, 171 insertions(+), 79 deletions(-) diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 8887624c8091..abe098ca05ac 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -339,7 +339,9 @@ python.debug.remote.name=PyRemoteDebug filetype.python.debug.remote.description=Remote debug python.sdk.no.interpreter.configured.for.module=No Python interpreter configured for {0} +python.sdk.interpreter.fix.already.in.progress=Interpreter configuration is in progress python.sdk.configure.python.interpreter=Configure Python interpreter +python.sdk.custom.environment=Custom Environment python.sdk.checking.existing.environments=Checking existing environments python.sdk.interpreter.settings=Interpreter settings python.sdk.pipenv.associated.with.another.project=Pipenv interpreter is associated with another project: ''{0}'' diff --git a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt index 5359b76d06c0..96bf014c096c 100644 --- a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt +++ b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt @@ -13,14 +13,19 @@ import com.intellij.openapi.util.NlsContexts import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.psi.PsiFile import com.intellij.ui.EditorNotifications +import com.intellij.ui.components.ActionLink import com.jetbrains.python.inspections.interpreter.InterpreterFix +import com.jetbrains.python.inspections.interpreter.BusyGuardExecutor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableStateFlow +import com.intellij.platform.util.coroutines.sync.OverflowSemaphore +import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus -import kotlinx.coroutines.future.asCompletableFuture import java.util.concurrent.CompletableFuture import java.util.concurrent.Executor import kotlin.time.Duration @@ -60,7 +65,7 @@ class PyAsyncFileInspectionRunner( private fun startComputation(module: Module): Deferred { val project = module.project - val deferred = project.service().scope.async { + val deferred = project.service().scope.async { withBackgroundProgress(project, progressTitle) { cacheLoader(module) } @@ -90,7 +95,7 @@ class PyAsyncFileInspectionRunner( // Must use a separate coroutine scope: invokeOnCompletion runs inline in the completing // coroutine's context, and EditorNotifications.getInstance() may need runBlocking for // service initialization, which fails inside an already-completed coroutine scope. - project.serviceIfCreated()?.scope?.launch { + project.serviceIfCreated()?.scope?.launch { EditorNotifications.getInstance(project).updateAllNotifications() } } @@ -106,13 +111,37 @@ private class CacheEvictingFix( private val fix: InterpreterFix, private val cacheEvictor: () -> Unit, ) : InterpreterFix { - override val name: String get() = fix.name - - override fun apply(module: Module, project: Project, psiFile: PsiFile) { - fix.apply(module, project, psiFile) - cacheEvictor() + override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink { + val link = fix.createActionLink(module, project, psiFile, executor) + link.addActionListener { cacheEvictor() } + return link } } +@ApiStatus.Internal @Service(Service.Level.PROJECT) -private class InspectionRunnerService(val scope: CoroutineScope) +class InterpreterFixExecutor(private val project: Project, internal val scope: CoroutineScope) : BusyGuardExecutor { + private val semaphore = OverflowSemaphore(permits = 1, overflow = BufferOverflow.DROP_LATEST) + private val _isBusy = MutableStateFlow(false) + override val isBusy: Boolean get() = _isBusy.value + + init { + scope.launch { + _isBusy.collect { EditorNotifications.getInstance(project).updateAllNotifications() } + } + } + + override fun execute(action: suspend () -> Unit) { + scope.launch { + semaphore.withPermit { + _isBusy.value = true + try { + action() + } + finally { + _isBusy.value = false + } + } + } + } +} diff --git a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt index 53262a053ed2..d37240cf2305 100644 --- a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt @@ -3,7 +3,11 @@ package com.jetbrains.python.inspections.interpreter import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.ide.DataManager import com.intellij.ide.actions.ShowSettingsUtilImpl +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.trace import com.intellij.openapi.module.Module @@ -14,6 +18,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.vfs.newvfs.RefreshQueue import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService import com.intellij.python.common.tools.ToolId import com.intellij.python.pyproject.model.api.ModuleCreateInfo @@ -24,23 +29,55 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable import com.jetbrains.python.Result import com.jetbrains.python.inspections.InspectionRunnerResult -import com.jetbrains.python.sdk.PySdkPopupFactory +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.ui.components.ActionLink +import com.intellij.ui.components.DropDownLink +import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.collectAddInterpreterActions +import com.intellij.openapi.util.use +import com.intellij.platform.ide.progress.withBackgroundProgress import com.jetbrains.python.sdk.configuration.CreateSdkInfo import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration import com.jetbrains.python.sdk.configuration.createSdk import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.sdk.switchToSdk import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService import com.jetbrains.python.sdk.setAssociationToModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.Nls +/** + * Executor that accepts at most one concurrent task. + * While a task is running, subsequent submissions are silently discarded. + * + * [isBusy] reflects the current state and can be used to update UI accordingly + * (e.g., replacing action links with a progress indicator). + */ +@ApiStatus.Internal +interface BusyGuardExecutor { + val isBusy: Boolean + fun execute(action: suspend () -> Unit) +} + +/** + * Provides an [ActionLink] for the "no Python interpreter" editor notification banner. + * + * Implementations are discovered asynchronously by [PyAsyncFileInspectionRunner][com.jetbrains.python.inspections.PyAsyncFileInspectionRunner] + * and rendered inside [PyInterpreterNotificationProvider]. + * Long-running work (SDK creation, tool installation) must be submitted through the supplied [BusyGuardExecutor] + * so that all notification panels share the same busy state. + */ @ApiStatus.Internal interface InterpreterFix { - val name: @Nls String - fun apply(module: Module, project: Project, psiFile: PsiFile) + fun createActionLink( + module: Module, + project: Project, + psiFile: PsiFile, + executor: BusyGuardExecutor, + ): ActionLink } class InterpreterSettingsQuickFix(private val myModule: Module?) : LocalQuickFix { @@ -102,19 +139,48 @@ private suspend fun getSuitableSdkFix( } } -private class ConfigureInterpreterFix : InterpreterFix { - override val name: String = PyBundle.message("python.sdk.configure.python.interpreter") +internal class ConfigureInterpreterFix : InterpreterFix { + override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink { + return DropDownLink(PyBundle.message("python.sdk.custom.environment")) { + val context = DataManager.getInstance().getDataContext(it) + createAddInterpreterPopup(module, context, executor) + } + } - override fun apply(module: Module, project: Project, psiFile: PsiFile) { - PySdkPopupFactory.createAndShow(module) + companion object { + fun createAddInterpreterPopup(module: Module, context: DataContext, executor: BusyGuardExecutor): JBPopup { + val currentSdk = module.pythonSdk + val group = DefaultActionGroup() + group.addAll(collectAddInterpreterActions(ModuleOrProject.ModuleAndProject(module)) { sdk -> + executor.execute { + withContext(Dispatchers.IO) { switchToSdk(module, sdk, currentSdk) } + } + }) + ActionManager.getInstance().getAction("Python.NewInterpreter.Extra")?.let { + group.add(it) + } + return JBPopupFactory.getInstance().createActionGroupPopup( + null, + group, + context, + JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, + false, + ) + } } } private class UseProvidedInterpreterFix(private val myModule: Module, private val myCreateSdkInfo: CreateSdkInfoWithTool) : InterpreterFix { - override val name: String = myCreateSdkInfo.createSdkInfo.intentionName - - override fun apply(module: Module, project: Project, psiFile: PsiFile) { - PyProjectSdkConfiguration.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo) + override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink { + return ActionLink(myCreateSdkInfo.createSdkInfo.intentionName) { + executor.execute { + val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myCreateSdkInfo.toolId.id) + withBackgroundProgress(project, myCreateSdkInfo.createSdkInfo.intentionName, false) { + lifetime.use { PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo) } + } + RefreshQueue.getInstance().refresh(recursive = false, files = ModuleRootManager.getInstance(myModule).contentRoots.toList()) + } + } } } @@ -123,10 +189,15 @@ private class SuggestToolInstallationFix( private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool, private val myTool: ToolId, ) : InterpreterFix { - override val name: String = myCreateSdkInfo.intentionName - - override fun apply(module: Module, project: Project, psiFile: PsiFile) { - PyProjectSdkConfiguration.installToolForInspection(myModule, myCreateSdkInfo, myTool) + override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink { + return ActionLink(myCreateSdkInfo.intentionName) { + executor.execute { + val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myTool.id) + withBackgroundProgress(project, myCreateSdkInfo.intentionName, false) { + lifetime.use { PyProjectSdkConfiguration.installToolAndShowErrorIfNeeded(myModule, myCreateSdkInfo.pathPersister, myCreateSdkInfo.toolToInstall) } + } + } + } } } diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt index fbcc36b07900..597e7a6a6619 100644 --- a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt +++ b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt @@ -1,6 +1,7 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.inspections.interpreter +import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtilCore @@ -14,8 +15,10 @@ import com.intellij.psi.search.GlobalSearchScope import com.intellij.python.pyproject.PY_PROJECT_TOML import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider +import com.intellij.util.ui.AsyncProcessIcon import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonFileType +import com.jetbrains.python.inspections.InterpreterFixExecutor import com.jetbrains.python.inspections.PyAsyncFileInspectionRunner import com.jetbrains.python.inspections.PyInspectionExtension import com.jetbrains.python.module.PyModuleService @@ -51,18 +54,25 @@ class PyInterpreterNotificationProvider : EditorNotificationProvider, DumbAware val interpreterFixes = asyncFileInspectionRunner.runInspection(module)?.takeIf { it.isNotEmpty() } ?: return null - return Function { fileEditor -> - val panel = EditorNotificationPanel(fileEditor, EditorNotificationPanel.Status.Warning).apply { - text = PyBundle.message("python.sdk.no.interpreter.configured.for.module", module.name) + val executor: BusyGuardExecutor = project.service() - interpreterFixes.forEach { fix -> - createActionLabel(fix.name) { - fix.apply(module, project, psiFile) + return Function { fileEditor -> + object : EditorNotificationPanel(fileEditor, Status.Warning) { + init { + text = PyBundle.message("python.sdk.no.interpreter.configured.for.module", module.name) + if (executor.isBusy) { + val label = javax.swing.JLabel(PyBundle.message("python.sdk.interpreter.fix.already.in.progress")) + label.foreground = com.intellij.util.ui.UIUtil.getInactiveTextColor() + myLinksPanel.add(label) + myLinksPanel.add(AsyncProcessIcon("interpreter fix")) + } + else { + interpreterFixes.forEach { fix -> + myLinksPanel.add(fix.createActionLink(module, project, psiFile, executor)) + } } } } - - panel } } } diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt index 988118125223..51ac54a26c00 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt @@ -6,20 +6,17 @@ import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service + import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.project.isNotificationSilentMode import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.use + import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider -import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.python.common.tools.ToolId + import com.intellij.python.community.services.systemPython.SystemPythonService -import com.intellij.ui.EditorNotifications import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonPluginDisposable import com.jetbrains.python.errorProcessing.PyResult @@ -32,38 +29,12 @@ import com.jetbrains.python.sdk.impl.PySdkBundle import com.jetbrains.python.sdk.installExecutableViaPythonScript import com.jetbrains.python.statistics.ConfiguredPythonInterpreterIdsHolder.Companion.SDK_HAS_BEEN_CONFIGURED_AS_THE_PROJECT_INTERPRETER import com.jetbrains.python.util.ShowingMessageErrorSync -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.nio.file.Path object PyProjectSdkConfiguration { - fun configureSdkUsingCreateSdkInfo(module: Module, createSdkInfoWithTool: CreateSdkInfoWithTool) { - val lifetime = suppressTipAndInspectionsFor(module, createSdkInfoWithTool.toolId.id) - - val project = module.project - project.service().scope.launch { - withBackgroundProgress(project, createSdkInfoWithTool.createSdkInfo.intentionName, false) { - lifetime.use { setSdkUsingCreateSdkInfo(module, createSdkInfoWithTool) } - } - } - } - - fun installToolForInspection(module: Module, createSdkInfo: CreateSdkInfo.WillInstallTool, toolId: ToolId) { - val lifetime = suppressTipAndInspectionsFor(module, toolId.id) - - val project = module.project - project.service().scope.launch { - withBackgroundProgress(project, createSdkInfo.intentionName, false) { - lifetime.use { installToolAndShowErrorIfNeeded(module, createSdkInfo.pathPersister, createSdkInfo.toolToInstall) } - } - - EditorNotifications.getInstance(project).updateAllNotifications() - } - } - - private suspend fun installToolAndShowErrorIfNeeded(module: Module, pathPersister: (Path) -> Unit, toolToInstall: String) { + internal suspend fun installToolAndShowErrorIfNeeded(module: Module, pathPersister: (Path) -> Unit, toolToInstall: String) { performToolInstallation(pathPersister, toolToInstall).errorOrNull?.also { ShowingMessageErrorSync.emit(it, module.project) } @@ -134,6 +105,3 @@ object PyProjectSdkConfiguration { } } } - -@Service(Service.Level.PROJECT) -private class SdkConfigurationService(val scope: CoroutineScope) diff --git a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt index 7d9cd8c3884c..32df1a7c8807 100644 --- a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt @@ -1,7 +1,6 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.venv.sdk.configuration -import com.intellij.openapi.application.EDT import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil @@ -77,7 +76,7 @@ internal class PyVenvSdkConfiguration : PyProjectSdkConfigurationExtension { getVirtualEnv(venvsInModule)?.refreshAndFindVirtualFile() } ?: return PyResult.failure(MessageError(PyBundle.message("sdk.cannot.find.venv.for.module"))) - val sdk = withContext(Dispatchers.EDT) { + val sdk = withContext(Dispatchers.IO) { SdkConfigurationUtil.setupSdk( PythonSdkUtil.getAllSdks().toTypedArray(), pythonBinary, diff --git a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt index 384683c7ed66..33fd7d541dfa 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt +++ b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt @@ -10,12 +10,13 @@ import com.intellij.testFramework.junit5.TestApplication import com.intellij.testFramework.junit5.fixture.moduleFixture import com.intellij.testFramework.junit5.fixture.projectFixture import com.jetbrains.python.inspections.interpreter.InterpreterFix +import com.jetbrains.python.inspections.interpreter.BusyGuardExecutor import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -50,7 +51,9 @@ class PyAsyncFileInspectionRunnerTest { barrier.complete(Unit) waitUntilAssertSucceeds(timeout = 5.seconds) { - assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name }) + val result = runner.runInspection(module) + assertNotNull(result) + assertEquals(expectedFixes.size, result!!.size) } } @@ -63,7 +66,9 @@ class PyAsyncFileInspectionRunnerTest { } waitUntilAssertSucceeds(timeout = 5.seconds) { - assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name }) + val result = runner.runInspection(module) + assertNotNull(result) + assertEquals(expectedFixes.size, result!!.size) } (1..100).map { @@ -74,7 +79,9 @@ class PyAsyncFileInspectionRunnerTest { } }.joinAll() - assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name }) + val result = runner.runInspection(module) + assertNotNull(result) + assertEquals(expectedFixes.size, result!!.size) assertEquals(1, callCount) } @@ -92,7 +99,9 @@ class PyAsyncFileInspectionRunnerTest { // Wait for the result of the first run waitUntilAssertSucceeds(timeout = 5.seconds) { - assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name }) + val result = runner.runInspection(module) + assertNotNull(result) + assertEquals(expectedFixes.size, result!!.size) assertEquals(1, callCount) } @@ -101,12 +110,16 @@ class PyAsyncFileInspectionRunnerTest { // Wait for the result of the second run waitUntilAssertSucceeds(timeout = 5.seconds) { - assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name }) + val result = runner.runInspection(module) + assertNotNull(result) + assertEquals(expectedFixes.size, result!!.size) assertEquals(2, callCount) } } } -private class TestInterpreterFix(override val name: String) : InterpreterFix { - override fun apply(module: Module, project: Project, psiFile: PsiFile) {} +private class TestInterpreterFix(val name: String) : InterpreterFix { + override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): com.intellij.ui.components.ActionLink { + return com.intellij.ui.components.ActionLink(name) {} + } } \ No newline at end of file