diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt index 6b3307e173f2..1b6e8aea8bb1 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt @@ -2,8 +2,6 @@ package com.jetbrains.python.newProjectWizard import com.intellij.facet.ui.ValidationResult -import com.intellij.ide.projectView.impl.AbstractProjectViewPane -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT import com.intellij.openapi.application.writeAction import com.intellij.openapi.components.Service @@ -17,15 +15,15 @@ import com.intellij.platform.DirectoryProjectGenerator import com.intellij.platform.ProjectGeneratorPeer import com.intellij.util.concurrency.annotations.RequiresEdt import com.jetbrains.python.Result +import com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode -import com.jetbrains.python.util.ErrorSink -import com.jetbrains.python.util.ShowingMessageErrorSync import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jetbrains.annotations.TestOnly /** * Extend this class to register a new project generator. @@ -33,27 +31,36 @@ import kotlinx.coroutines.withContext * [typeSpecificUI] is a UI to display these settings and bind then using Kotlin DSL UI * [allowedInterpreterTypes] limits a list of allowed interpreters (all interpreters are allowed by default) * [newProjectName] is a default name of the new project ([getName]Project is default) - * [expandProjectAfterCreation]: expand project tree view on the left side of IDE + * + * To test this class, see [setUiServices] */ abstract class PyV3ProjectBaseGenerator( private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS, private val typeSpecificUI: PyV3ProjectTypeSpecificUI?, private val allowedInterpreterTypes: Set? = null, - private val errorSink: ErrorSink = ShowingMessageErrorSync, private val _newProjectName: @NlsSafe String? = null, - private val expandProjectAfterCreation: Boolean = !ApplicationManager.getApplication().isHeadlessEnvironment, ) : DirectoryProjectGenerator { private val baseSettings = PyV3BaseProjectSettings() + private var uiServices: PyV3UIServices = PyV3UIServicesProd val newProjectName: @NlsSafe String get() = _newProjectName ?: "${name.replace(" ", "")}Project" + /** + * Run this method as before any other to substitute services with mock for tests + */ + @TestOnly + fun setUiServices(uiServices: PyV3UIServices) { + this.uiServices = uiServices + } + + @RequiresEdt override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) { val coroutineScope = project.service().coroutineScope coroutineScope.launch { val sdk = settings.generateAndGetSdk(module, baseDir).getOrElse { withContext(Dispatchers.EDT) { - errorSink.emit(it.localizedMessage) // Show error generation to user + uiServices.errorSink.emit(it.localizedMessage) // Show error generation to user } return@launch // Since we failed to generate project, we do not need to go any further } @@ -67,19 +74,15 @@ abstract class PyV3ProjectBaseGenerator pane.tree }.expandRow(0) - } override fun createPeer(): ProjectGeneratorPeer = - PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes) + PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes, uiServices) override fun validate(baseDirPath: String): ValidationResult = when (val pathOrError = validatePath(baseDirPath)) { diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3UIServices.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3UIServices.kt new file mode 100644 index 000000000000..91a5e4f7e1bd --- /dev/null +++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3UIServices.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.newProjectWizard + +import com.intellij.openapi.project.Project +import com.jetbrains.python.util.ErrorSink +import kotlinx.coroutines.CoroutineScope +import javax.swing.JComponent + +/** + * Various services that aren't accessible in tests. + * There is [com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd] used in production, and you should never + * touch it, unless you test your [PyV3ProjectBaseGenerator]. In this case, use [PyV3ProjectBaseGenerator.setUiServices] in tests. + * There is an inheritor + */ +interface PyV3UIServices { + /** + * Runs [code] when [component] is visible + */ + fun runWhenComponentDisplayed(component: JComponent, code: suspend CoroutineScope.() -> Unit) + + /** + * Send user errors here: they will arrive to user + */ + val errorSink: ErrorSink + + /** + * Expand project tree on the left side of IDE + */ + suspend fun expandProjectTreeView(project: Project) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt index e0188c0613e8..d01dcee40139 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt @@ -8,6 +8,7 @@ import com.intellij.platform.ProjectGeneratorPeer import com.jetbrains.python.newProjectWizard.PyV3BaseProjectSettings import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI +import com.jetbrains.python.newProjectWizard.PyV3UIServices import com.jetbrains.python.newProjectWizard.impl.projectPath.ProjectPathImpl import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode import javax.swing.JComponent @@ -16,13 +17,14 @@ internal class PyV3GeneratorPeer, TYPE_SPECIFIC_SETTINGS>?, private val allowedInterpreterTypes: Set?, + private val uiServices: PyV3UIServices, ) : ProjectGeneratorPeer { private val settings = baseSettings private lateinit var panel: PyV3UI<*> override fun getComponent(projectPathField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent { - panel = PyV3UI(settings, ProjectPathImpl(projectPathField), specificUiAndSettings, allowedInterpreterTypes) + panel = PyV3UI(settings, ProjectPathImpl(projectPathField, uiServices), specificUiAndSettings, allowedInterpreterTypes) return panel.mainPanel } diff --git a/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3UIServicesProd.kt b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3UIServicesProd.kt new file mode 100644 index 000000000000..b8888900fee1 --- /dev/null +++ b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3UIServicesProd.kt @@ -0,0 +1,28 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.newProjectWizard.impl + +import com.intellij.ide.projectView.impl.AbstractProjectViewPane +import com.intellij.openapi.application.EDT +import com.intellij.openapi.project.Project +import com.intellij.util.ui.showingScope +import com.jetbrains.python.newProjectWizard.PyV3UIServices +import com.jetbrains.python.util.ErrorSink +import com.jetbrains.python.util.ShowingMessageErrorSync +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.swing.JComponent + +internal object PyV3UIServicesProd : PyV3UIServices { + override fun runWhenComponentDisplayed(component: JComponent, code: suspend CoroutineScope.() -> Unit) { + component.showingScope("..") { + code() + } + } + + override val errorSink: ErrorSink = ShowingMessageErrorSync + + override suspend fun expandProjectTreeView(project: Project): Unit = withContext(Dispatchers.EDT) { + AbstractProjectViewPane.EP.getExtensions(project).firstNotNullOf { pane -> pane.tree }.expandRow(0) + } +} diff --git a/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/ProjectPathImpl.kt b/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/ProjectPathImpl.kt index 7cb24f540196..6272622dab94 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/ProjectPathImpl.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/ProjectPathImpl.kt @@ -5,28 +5,24 @@ import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.NlsSafe import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.showingScope +import com.jetbrains.python.newProjectWizard.PyV3UIServices import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider -import kotlinx.coroutines.CoroutineScope +import org.jetbrains.annotations.ApiStatus /** * Wraps [field] that represents project path, and emits [projectPathFlows] out of it * * [onProjectFileNameChanged] allows caller to receive every [ProjectPathFlows.projectName] event as long as [field] is visible - * [fieldShowingScope] shouldn't be changes (except for test purposes) */ +@ApiStatus.Internal class ProjectPathImpl( private val field: TextFieldWithBrowseButton, - private val fieldShowingScope: FieldShowingScopeRunner = object : FieldShowingScopeRunner { - override fun onShowingScope(code: suspend CoroutineScope.() -> Unit) { - field.showingScope("On Project Changed") { - code() - } - } - }, + private val uiServices: PyV3UIServices, ) : ProjectPathProvider { + + private val listener = DocumentListenerToFlowAdapter(field) override val projectPathFlows: ProjectPathFlows = ProjectPathFlows.create(listener.flow) @@ -39,15 +35,8 @@ class ProjectPathImpl( @RequiresEdt override fun onProjectFileNameChanged(code: suspend (projectPathName: @NlsSafe String) -> Unit) { - fieldShowingScope.onShowingScope { + uiServices.runWhenComponentDisplayed(field) { projectPathFlows.projectName.collect(code) } } - - - companion object { - fun interface FieldShowingScopeRunner { - fun onShowingScope(code: (suspend CoroutineScope.() -> Unit)) - } - } } diff --git a/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/package-info.java b/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/package-info.java new file mode 100644 index 000000000000..b370ac7a9815 --- /dev/null +++ b/python/src/com/jetbrains/python/newProjectWizard/impl/projectPath/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@ApiStatus.Internal +package com.jetbrains.python.newProjectWizard.impl.projectPath; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/util/ErrorSink.kt b/python/src/com/jetbrains/python/util/ErrorSink.kt index 984799466d5b..16f711d98594 100644 --- a/python/src/com/jetbrains/python/util/ErrorSink.kt +++ b/python/src/com/jetbrains/python/util/ErrorSink.kt @@ -53,7 +53,7 @@ typealias ErrorSink = FlowCollector<@NlsSafe String> /** * Displays error with a message box and writes it to a log. */ -object ShowingMessageErrorSync : ErrorSink { +internal object ShowingMessageErrorSync : ErrorSink { override suspend fun emit(value: @NlsSafe String) { withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) { thisLogger().warn(value)