Introduce PyV3UIServices to uncouple PyV3 project generation machinery from Swing UI.

With all of that it would be possible to test the whole new project generation process

GitOrigin-RevId: 996b5cb234911a752944ff99bf39faaf448f9c89
This commit is contained in:
Ilya.Kazakevich
2024-11-28 23:18:07 +00:00
committed by intellij-monorepo-bot
parent fc77b60658
commit 1e5e5b9bff
7 changed files with 93 additions and 36 deletions
@@ -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<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS,
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
private val errorSink: ErrorSink = ShowingMessageErrorSync,
private val _newProjectName: @NlsSafe String? = null,
private val expandProjectAfterCreation: Boolean = !ApplicationManager.getApplication().isHeadlessEnvironment,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings> {
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<MyService>().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<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
// Project view must be expanded (PY-75909) but it can't be unless it contains some files.
// Either base settings (which create venv) might generate some or type specific settings (like Django) may.
// So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
ensureProjectViewExpanded(project)
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure { errorSink.emit(it.localizedMessage) }
ensureProjectViewExpanded(project)
uiServices.expandProjectTreeView(project)
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure { uiServices.errorSink.emit(it.localizedMessage) }
uiServices.expandProjectTreeView(project)
}
}
private suspend fun ensureProjectViewExpanded(project: Project): Unit = withContext(Dispatchers.EDT) {
if (!expandProjectAfterCreation) return@withContext
AbstractProjectViewPane.EP.getExtensions(project).firstNotNullOf { pane -> pane.tree }.expandRow(0)
}
override fun createPeer(): ProjectGeneratorPeer<PyV3BaseProjectSettings> =
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)) {
@@ -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)
}
@@ -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 : PyV3ProjectTypeSpecifi
baseSettings: PyV3BaseProjectSettings,
private val specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>?,
private val uiServices: PyV3UIServices,
) : ProjectGeneratorPeer<PyV3BaseProjectSettings> {
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
}
@@ -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)
}
}
@@ -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))
}
}
}
@@ -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;
@@ -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)