mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
fc77b60658
commit
1e5e5b9bff
@@ -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)
|
||||
}
|
||||
}
|
||||
+7
-18
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user