mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python] PY-79486, PY-86454: WIP: Automatic SDK configuration, see ModuleConfigurationMode
Two modes are described in `ModuleConfigurationMode` GitOrigin-RevId: bf6508571fd2f31ee87dd4b52c293155583c6eea
This commit is contained in:
committed by
intellij-monorepo-bot
parent
e9babbd19f
commit
e922274b64
@@ -85,6 +85,8 @@ jvm_library(
|
||||
"//platform/testFramework/junit5/projectStructure",
|
||||
"//platform/testFramework/junit5/projectStructure:projectStructure_test_lib",
|
||||
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
|
||||
"//platform/testFramework",
|
||||
"//platform/testFramework:testFramework_test_lib",
|
||||
],
|
||||
exports = ["@lib//:tuweni-toml"]
|
||||
)
|
||||
|
||||
@@ -38,5 +38,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5.projectStructure" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.intellij.python.pyproject.model.internal.autoImportBridge
|
||||
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.FSWalkInfo
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystem
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class FsInfoStorage(private val projectRootDir: Path) {
|
||||
private val updateMut = Mutex()
|
||||
|
||||
private var cache: FSWalkInfo? = null
|
||||
|
||||
|
||||
suspend fun getFsInfo(forceRefresh: Boolean): FSWalkInfo {
|
||||
updateMut.withLock {
|
||||
val cache = cache
|
||||
if (cache != null && !forceRefresh) {
|
||||
return cache
|
||||
}
|
||||
val fsInfo = walkFileSystem(projectRootDir)
|
||||
this.cache = fsInfo
|
||||
return fsInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-9
@@ -1,19 +1,25 @@
|
||||
package com.intellij.python.pyproject.model.internal.autoImportBridge
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.externalSystem.autoimport.*
|
||||
import com.intellij.openapi.externalSystem.model.ProjectSystemId
|
||||
import com.intellij.openapi.progress.runBlockingMaybeCancellable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.platform.backend.observation.launchTracked
|
||||
import com.intellij.project.stateStore
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
|
||||
import com.intellij.python.pyproject.model.internal.PyProjectTomlBundle
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemNoTomlContent
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemWithTomlContent
|
||||
import com.intellij.python.pyproject.model.internal.workspaceBridge.rebuildProjectModel
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.messages.Topic
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
@@ -21,17 +27,17 @@ import kotlin.io.path.pathString
|
||||
|
||||
internal class PyExternalSystemProjectAware private constructor(
|
||||
private val project: Project,
|
||||
projectRootDir: Path,
|
||||
private val projectRootDir: Path,
|
||||
) : ExternalSystemProjectAware {
|
||||
override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(SYSTEM_ID, projectRootDir.pathString)
|
||||
|
||||
private val fsInfo = FsInfoStorage(projectRootDir)
|
||||
|
||||
@get:RequiresBackgroundThread
|
||||
override val settingsFiles: Set<String>
|
||||
get() = runBlockingMaybeCancellable {
|
||||
val fsInfo = fsInfo.getFsInfo(forceRefresh = true)
|
||||
return@runBlockingMaybeCancellable fsInfo.tomlFiles.keys.map { it.pathString }.toSet()
|
||||
// We do not need file content: only names here.
|
||||
val fsInfo = walkFileSystemNoTomlContent(projectRootDir)
|
||||
return@runBlockingMaybeCancellable fsInfo.rawTomlFiles.map { it.pathString }.toSet()
|
||||
}
|
||||
|
||||
override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) {
|
||||
@@ -39,10 +45,18 @@ internal class PyExternalSystemProjectAware private constructor(
|
||||
}
|
||||
|
||||
override fun reloadProject(context: ExternalSystemProjectReloadContext) {
|
||||
project.service<PyProjectAutoImportService>().scope.launchTracked {
|
||||
reloadProjectImpl()
|
||||
}
|
||||
|
||||
internal fun reloadProjectImpl() {
|
||||
project.service<PyExternalSystemProjectAwareService>().scope.launchTracked {
|
||||
writeAction {
|
||||
// We might get stale files otherwise
|
||||
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
||||
}
|
||||
project.messageBus.syncAndPreloadPublisher(PROJECT_AWARE_TOPIC).apply {
|
||||
try {
|
||||
val files = fsInfo.getFsInfo(forceRefresh = context.hasUndefinedModifications)
|
||||
val files = walkFileSystemWithTomlContent(projectRootDir)
|
||||
this.onProjectReloadStart()
|
||||
rebuildProjectModel(project, files)
|
||||
this.onProjectReloadFinish(ExternalSystemRefreshStatus.SUCCESS)
|
||||
@@ -65,9 +79,14 @@ internal class PyExternalSystemProjectAware private constructor(
|
||||
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* [project] can't be default, be sure to check it
|
||||
*/
|
||||
internal suspend fun create(project: Project): PyExternalSystemProjectAware {
|
||||
assert(!project.isDefault) { "Default project not supported" }
|
||||
val baseDir = withContext(Dispatchers.IO) {
|
||||
project.guessProjectDir()?.toNioPath() ?: error("Project $project has no base dir")
|
||||
// guessPath doesn't work: it returns first module path
|
||||
project.stateStore.projectBasePath
|
||||
}
|
||||
return PyExternalSystemProjectAware(project, baseDir)
|
||||
}
|
||||
@@ -83,4 +102,7 @@ private val PROJECT_AWARE_TOPIC: Topic<ExternalSystemProjectListener> =
|
||||
Topic(ExternalSystemProjectListener::class.java, Topic.BroadcastDirection.NONE)
|
||||
|
||||
@Topic.ProjectLevel
|
||||
private val MODEL_REBUILD: Topic<ModelRebuiltListener> = Topic(ModelRebuiltListener::class.java, Topic.BroadcastDirection.NONE)
|
||||
internal val MODEL_REBUILD: Topic<ModelRebuiltListener> = Topic(ModelRebuiltListener::class.java, Topic.BroadcastDirection.NONE)
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class PyExternalSystemProjectAwareService(val scope: CoroutineScope)
|
||||
+8
-2
@@ -5,10 +5,16 @@ import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* [project] can't be default, check for it
|
||||
*/
|
||||
@Service(Service.Level.PROJECT)
|
||||
internal class PyProjectAutoImportService(private val project: Project, internal val scope: CoroutineScope) : Disposable {
|
||||
internal class PyProjectAutoImportService(private val project: Project) : Disposable {
|
||||
init {
|
||||
assert(!project.isDefault) { "Default project not supported" }
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private lateinit var projectId: ExternalSystemProjectId
|
||||
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ internal class PyProjectSyncActivity : ProjectActivity {
|
||||
}
|
||||
|
||||
override suspend fun execute(project: Project) {
|
||||
if (project.isDefault) return // Service doesn't support default project
|
||||
project.service<PyProjectAutoImportService>().start()
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
|
||||
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
|
||||
import com.intellij.python.pyproject.model.internal.projectModelEnabled
|
||||
|
||||
@@ -12,14 +11,15 @@ internal class PyProjectTomlSyncAction : AnAction() {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
if (!projectModelEnabled) {
|
||||
if (!projectModelEnabled || project.isDefault) { // Service doesn't support default project
|
||||
return
|
||||
}
|
||||
project.service<PyProjectAutoImportService>().refresh()
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = e.project != null && projectModelEnabled
|
||||
val project = e.project
|
||||
e.presentation.isEnabledAndVisible = project != null && projectModelEnabled && !project.isDefault
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package com.intellij.python.pyproject.model.internal.pyProjectToml
|
||||
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.jetbrains.python.venvReader.Directory
|
||||
import java.nio.file.Path
|
||||
|
||||
internal data class FSWalkInfo(val tomlFiles: Map<Path, PyProjectToml>, val excludeDir: Set<Directory>)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.intellij.python.pyproject.model.internal.pyProjectToml
|
||||
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.jetbrains.python.venvReader.Directory
|
||||
import java.nio.file.Path
|
||||
|
||||
// Files with toml content
|
||||
internal data class FSWalkInfoWithToml(val tomlFiles: Map<Path, PyProjectToml>, val excludeDir: Set<Directory>)
|
||||
|
||||
|
||||
// Files only
|
||||
internal data class FsWalkInfoNoToml(val rawTomlFiles: List<Path>, val excludedDirs: List<Directory>)
|
||||
+22
-12
@@ -1,6 +1,7 @@
|
||||
package com.intellij.python.pyproject.model.internal.pyProjectToml
|
||||
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.python.pyproject.model.spi.ProjectName
|
||||
@@ -24,20 +25,33 @@ import kotlin.io.path.visitFileTree
|
||||
|
||||
// Tools to walk FS and parse pyproject.toml
|
||||
|
||||
internal suspend fun walkFileSystem(root: Directory): FSWalkInfo {
|
||||
val files = ArrayList<Path>(10)
|
||||
val excludeDir = ArrayList<Directory>(10)
|
||||
internal suspend fun walkFileSystemWithTomlContent(root: Directory): FSWalkInfoWithToml {
|
||||
val (rawTomlFiles, excludedDirs) = walkFileSystemNoTomlContent(root)
|
||||
|
||||
// TODO: with a big number of files, use `chunk` to parse them concurrently
|
||||
val tomlFiles = rawTomlFiles.map { file ->
|
||||
val toml = readFile(file) ?: return@map null
|
||||
file to toml
|
||||
}.filterNotNull().toMap()
|
||||
return FSWalkInfoWithToml(tomlFiles = tomlFiles, excludedDirs.toSet())
|
||||
}
|
||||
|
||||
internal suspend fun walkFileSystemNoTomlContent(
|
||||
root: Directory,
|
||||
): FsWalkInfoNoToml {
|
||||
val excludedDirs = ArrayList<Directory>(10)
|
||||
val rawTomlFiles = ArrayList<Path>(10)
|
||||
withContext(Dispatchers.IO) {
|
||||
root.visitFileTree {
|
||||
onVisitFile { file, _ ->
|
||||
if (file.name == PY_PROJECT_TOML) {
|
||||
files.add(file)
|
||||
rawTomlFiles.add(file)
|
||||
}
|
||||
return@onVisitFile FileVisitResult.CONTINUE
|
||||
}
|
||||
onPostVisitDirectory { directory, _ ->
|
||||
return@onPostVisitDirectory if (directory.name.startsWith(".")) {
|
||||
excludeDir.add(directory)
|
||||
excludedDirs.add(directory)
|
||||
FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
else {
|
||||
@@ -46,15 +60,11 @@ internal suspend fun walkFileSystem(root: Directory): FSWalkInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: with a big number of files, use `chunk` to parse them concurrently
|
||||
val tomlFiles = files.map { file ->
|
||||
val toml = readFile(file) ?: return@map null
|
||||
file to toml
|
||||
}.filterNotNull().toMap()
|
||||
return FSWalkInfo(tomlFiles = tomlFiles, excludeDir.toSet())
|
||||
return FsWalkInfoNoToml(rawTomlFiles = rawTomlFiles, excludedDirs = excludedDirs)
|
||||
}
|
||||
|
||||
suspend fun getProjectStructureDefault(
|
||||
|
||||
suspend fun getProjectStructureDefault(
|
||||
entries: Map<ProjectName, PyProjectTomlProject>,
|
||||
rootIndex: Map<Directory, ProjectName>,
|
||||
): ProjectStructureInfo = withContext(Dispatchers.Default) {
|
||||
|
||||
+31
-7
@@ -13,7 +13,7 @@ import com.intellij.platform.workspace.storage.url.VirtualFileUrlManager
|
||||
import com.intellij.python.common.tools.ToolId
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.python.pyproject.model.internal.PyProjectTomlBundle
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.FSWalkInfo
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.FSWalkInfoWithToml
|
||||
import com.intellij.python.pyproject.model.internal.pyProjectToml.getProjectStructureDefault
|
||||
import com.intellij.python.pyproject.model.spi.ProjectName
|
||||
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
|
||||
@@ -21,6 +21,7 @@ import com.intellij.python.pyproject.model.spi.Tool
|
||||
import com.intellij.python.pyproject.model.spi.WorkspaceName
|
||||
import com.intellij.workspaceModel.ide.impl.legacyBridge.facet.FacetModelBridge.Companion.findFacet
|
||||
import com.intellij.workspaceModel.ide.impl.legacyBridge.sdk.SdkBridgeImpl.Companion.findSdkEntity
|
||||
import com.intellij.workspaceModel.ide.isEqualOrParentOf
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.facet.PythonFacetSettings
|
||||
import com.jetbrains.python.venvReader.Directory
|
||||
@@ -36,19 +37,41 @@ import kotlin.io.path.name
|
||||
// Workspace adapter functions
|
||||
|
||||
|
||||
internal suspend fun rebuildProjectModel(project: Project, files: FSWalkInfo) {
|
||||
internal suspend fun rebuildProjectModel(project: Project, files: FSWalkInfoWithToml) {
|
||||
changeWorkspaceMutex.withLock {
|
||||
val entries = generatePyProjectTomlEntries(files)
|
||||
val (entries, excludeDirs) = generatePyProjectTomlEntries(files)
|
||||
val newStorage = createEntityStorage(entries, project.workspaceModel.getVirtualFileUrlManager())
|
||||
|
||||
project.workspaceModel.update(PyProjectTomlBundle.message("action.PyProjectTomlSyncAction.description")) { currentStorage -> // Fake module entity is added by default if nothing was discovered
|
||||
val workspaceModel = project.workspaceModel
|
||||
workspaceModel.update(PyProjectTomlBundle.message("action.PyProjectTomlSyncAction.description")) { currentStorage -> // Fake module entity is added by default if nothing was discovered
|
||||
removeFakeModuleEntity(currentStorage, entries.map { it.name.name }.toSet())
|
||||
// TODO: Store old module->SDK, so we can restoe it even when modules are destroyed
|
||||
relocateUserDefinedModuleSdk(currentStorage) {
|
||||
currentStorage.replaceBySource({ it is PyProjectTomlEntitySource }, newStorage)
|
||||
|
||||
// Exclude dirs
|
||||
if (excludeDirs.isEmpty()) return@relocateUserDefinedModuleSdk
|
||||
val modules = currentStorage.entities(ModuleEntity::class.java).toList()
|
||||
for (excludedRoot in excludeDirs.map { it.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) }) {
|
||||
currentStorage.excludeRoot(excludedRoot, modules)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableEntityStorage.excludeRoot(rootToExclude: VirtualFileUrl, modules: List<ModuleEntity>) {
|
||||
for (moduleEntry in modules) {
|
||||
for (rootEntity in moduleEntry.contentRoots) {
|
||||
if (rootEntity.url.isEqualOrParentOf(rootToExclude) && rootToExclude !in rootEntity.excludedUrls.map { it.url }) {
|
||||
modifyContentRootEntity(rootEntity) {
|
||||
excludedUrls = excludedUrls + listOf(
|
||||
ExcludeUrlEntity(rootToExclude, entitySource))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,9 +112,10 @@ internal fun relocateUserDefinedModuleSdk(storage: MutableEntityStorage, transfe
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: DOC
|
||||
private suspend fun generatePyProjectTomlEntries(
|
||||
fsInfo: FSWalkInfo,
|
||||
): Set<PyProjectTomlBasedEntryImpl> = withContext(Dispatchers.Default) {
|
||||
fsInfo: FSWalkInfoWithToml,
|
||||
): Pair<Set<PyProjectTomlBasedEntryImpl>, Set<Directory>> = withContext(Dispatchers.Default) {
|
||||
val (files, allExcludeDirs) = fsInfo
|
||||
val entries = ArrayList<PyProjectTomlBasedEntryImpl>()
|
||||
val usedNamed = mutableSetOf<String>()
|
||||
@@ -148,7 +172,7 @@ private suspend fun generatePyProjectTomlEntries(
|
||||
entriesByName[member]!!.relationsWithTools.add(PyProjectTomlToolRelation.WorkspaceMember(tool.id, workspace))
|
||||
}
|
||||
}
|
||||
return@withContext entries.toSet()
|
||||
return@withContext Pair(entries.toSet(), allExcludeDirs)
|
||||
}
|
||||
|
||||
private suspend fun getNameFromEP(projectToml: PyProjectToml): Pair<Tool, @NlsSafe String>? = withContext(Dispatchers.Default) {
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.intellij.python.junit5Tests.unit.pyproject
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
|
||||
import com.intellij.python.pyproject.model.internal.autoImportBridge.MODEL_REBUILD
|
||||
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyExternalSystemProjectAware
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
import com.intellij.testFramework.junit5.fixture.tempPathFixture
|
||||
import com.intellij.util.io.write
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Timeout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
|
||||
@Timeout(TIMEOUT_MIN.toLong(), unit = TimeUnit.MINUTES)
|
||||
@TestApplication
|
||||
class PyExternalSystemProjectAwareTest {
|
||||
private val members = arrayOf("foo", "bar").sortedArray()
|
||||
|
||||
private val pathFixture = tempPathFixture()
|
||||
private val projectFixture = projectFixture(pathFixture)
|
||||
|
||||
@BeforeEach
|
||||
fun prepareWorkspace(): Unit = timeoutRunBlocking {
|
||||
val root = pathFixture.get()
|
||||
for (member in members) {
|
||||
root.resolve(member).resolve(PY_PROJECT_TOML).write("""
|
||||
[project]
|
||||
name = "$member"
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testBuildProject(@TestDisposable disposable: Disposable): Unit = timeoutRunBlocking(TIMEOUT_MIN.minutes) {
|
||||
val sut = PyExternalSystemProjectAware.create(projectFixture.get())
|
||||
val files = withContext(Dispatchers.IO) {
|
||||
sut.settingsFiles
|
||||
}
|
||||
Assertions.assertEquals(members.size, files.size, "Wrong number of toml files")
|
||||
|
||||
sut.reloadProjectImpl()
|
||||
|
||||
val m = Mutex(locked = true)
|
||||
projectFixture.get().messageBus.connect(disposable).subscribe(MODEL_REBUILD, ModelRebuiltListener { project ->
|
||||
try {
|
||||
val moduleNames = project.modules.map { it.name }.sorted().toTypedArray()
|
||||
Assertions.assertArrayEquals(members, moduleNames, "Wrong modules created")
|
||||
}
|
||||
finally {
|
||||
m.unlock()
|
||||
}
|
||||
})
|
||||
m.lock()
|
||||
}
|
||||
}
|
||||
|
||||
private const val TIMEOUT_MIN = 2
|
||||
@@ -5,5 +5,6 @@
|
||||
<module name="intellij.platform.testFramework.junit5"/>
|
||||
<module name="intellij.python.community.junit5Tests.framework._test"/>
|
||||
<module name="intellij.platform.testFramework.junit5.projectStructure"/>
|
||||
<module name="intellij.platform.testFramework"/>
|
||||
</dependencies>
|
||||
</idea-plugin>
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
<idea-plugin visibility="private">
|
||||
<idea-plugin visibility="internal">
|
||||
<dependencies>
|
||||
<module name="intellij.platform.rpc"/>
|
||||
<module name="intellij.python.pyproject"/>
|
||||
@@ -11,7 +11,7 @@
|
||||
</dependencies>
|
||||
<resource-bundle>messages.PySdkConfiguratorBundle</resource-bundle>
|
||||
<projectListeners>
|
||||
<listener class="com.intellij.python.sdkConfigurator.backend.impl.platformBridge.ModelRebuiltListenerImpl"
|
||||
<listener class="com.intellij.python.sdkConfigurator.backend.impl.platformBridge.ModelRebuiltListenerImplToConfigureSdk"
|
||||
topic="com.intellij.python.pyproject.model.api.ModelRebuiltListener"/>
|
||||
</projectListeners>
|
||||
<extensions defaultExtensionNs="com.intellij.platform">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.intellij.python.sdkConfigurator.backend.impl
|
||||
|
||||
/**
|
||||
* SDK for project modules can be configured in two different modes (see PY-86454)
|
||||
*/
|
||||
internal enum class ModuleConfigurationMode {
|
||||
/**
|
||||
* Do not bother user with questions, do not touch filesystem.
|
||||
* For one-module project, configure SDK if files (i.e. venv) already exist.
|
||||
* For multi-module only set SDK for workspace members if workspace parent SDK is set.
|
||||
*
|
||||
* In other words: No more than 1 SDK could be created. No files should be created.
|
||||
*/
|
||||
AUTOMATIC,
|
||||
|
||||
/**
|
||||
* Show user list of modules without SDK, ask them which to configure, and do so.
|
||||
*/
|
||||
INTERACTIVE
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.intellij.python.sdkConfigurator.backend.impl
|
||||
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.trace
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessModuleDir
|
||||
@@ -31,9 +32,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Configures SDK for modules in [project].
|
||||
* Configures SDK for modules in [project] in [ModuleConfigurationMode.INTERACTIVE] mode.
|
||||
*
|
||||
* 1. Create instance with [create]
|
||||
* 2. Ask use to choose from [modulesDTO]
|
||||
@@ -83,13 +85,15 @@ internal class ModulesSdkConfigurator private constructor(
|
||||
.toList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal companion object {
|
||||
|
||||
/**
|
||||
* Create instance and save in [project]
|
||||
* Create instance and save in [project], see class doc
|
||||
*/
|
||||
suspend fun create(project: Project): ModulesSdkConfigurator = ModulesSdkConfigurator(project, getModulesWithoutSDKCreateInfo(project), PathShortener.create(project)).also {
|
||||
project.putUserData(key, it)
|
||||
}
|
||||
suspend fun create(project: Project): ModulesSdkConfigurator =
|
||||
ModulesSdkConfigurator(project, getModulesWithoutSDKCreateInfo(project), PathShortener.create(project)).also {
|
||||
project.putUserData(key, it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance from project and **clear it**
|
||||
@@ -101,49 +105,22 @@ internal class ModulesSdkConfigurator private constructor(
|
||||
return instance
|
||||
}
|
||||
|
||||
private suspend fun getModulesWithoutSDKCreateInfo(project: Project): Map<ModuleName, ModuleCreateInfo> = withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) {
|
||||
val tools = PyProjectSdkConfigurationExtension.createMap()
|
||||
val now = System.currentTimeMillis()
|
||||
val resultDef = project.modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.map { module ->
|
||||
async {
|
||||
val moduleInfo = getModuleInfo(module, tools) ?: return@async null
|
||||
Pair(module, moduleInfo)
|
||||
}
|
||||
}
|
||||
val result = resultDef.awaitAll().filterNotNull()
|
||||
logger.debug { "SDKs calculated in ${System.currentTimeMillis() - now}ms" }
|
||||
result.associate { (module, createInfoAndDTO) ->
|
||||
Pair(module.name, createInfoAndDTO)
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = fileLogger()
|
||||
|
||||
private sealed interface ModuleCreateInfo {
|
||||
data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId, val moduleDir: Directory?) : ModuleCreateInfo
|
||||
data class SameAs(val parentModuleName: ModuleName) : ModuleCreateInfo
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getModuleInfo(module: Module, configuratorsByTool: Map<ToolId, PyProjectSdkConfigurationExtension>): ModuleCreateInfo? = // Save on module level
|
||||
when (val r = module.suggestSdk()) {
|
||||
is SuggestedSdk.PyProjectIndependent -> {
|
||||
val tools = r.preferTools.map { configuratorsByTool[it]!! }
|
||||
tools.firstNotNullOfOrNull { tool ->
|
||||
val createInfo = (tool.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(module)
|
||||
?: tool.checkEnvironmentAndPrepareSdkCreator(module)) ?: return@firstNotNullOfOrNull null
|
||||
CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO(r.moduleDir)
|
||||
private suspend fun getModulesWithoutSDKCreateInfo(project: Project): Map<ModuleName, ModuleCreateInfo> =
|
||||
withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) {
|
||||
val tools = PyProjectSdkConfigurationExtension.createMap()
|
||||
val now = System.currentTimeMillis()
|
||||
val resultDef = project.modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.map { module ->
|
||||
async {
|
||||
val moduleInfo = getModuleInfo(module, tools) ?: return@async null
|
||||
Pair(module, moduleInfo)
|
||||
}
|
||||
}
|
||||
is SuggestedSdk.SameAs -> {
|
||||
ModuleCreateInfo.SameAs(r.parentModule.name)
|
||||
val result = resultDef.awaitAll().filterNotNull()
|
||||
logger.debug { "SDKs calculated in ${System.currentTimeMillis() - now}ms" }
|
||||
result.associate { (module, createInfoAndDTO) ->
|
||||
Pair(module.name, createInfoAndDTO)
|
||||
}
|
||||
null -> null
|
||||
} // No tools or not pyproject.toml at all? Use EP as a fallback
|
||||
?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull()?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO(module.guessModuleDir()?.toNioPath()) }
|
||||
|
||||
|
||||
private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo = ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -204,3 +181,90 @@ internal class ModulesSdkConfigurator private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See [ModuleConfigurationMode.AUTOMATIC]
|
||||
*/
|
||||
@ApiStatus.Internal // Opened for tests only: we can't put tests here because configurators are in communuty.impl
|
||||
suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Dispatchers.Default) {
|
||||
val modules = project.modules
|
||||
|
||||
when (modules.size) {
|
||||
0 -> return@withContext
|
||||
1 -> { // Single-module project, associate first module only
|
||||
val module = modules.first()
|
||||
if (PythonSdkUtil.findPythonSdk(module) != null) return@withContext // Already has SDK
|
||||
when (val moduleInfo = getModuleInfo(module)) {
|
||||
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
|
||||
when (val info = moduleInfo.createSdkInfo) {
|
||||
is CreateSdkInfo.ExistingEnv -> {
|
||||
when (val r = info.sdkCreator(false)) {
|
||||
is Result.Failure -> {
|
||||
logger.trace { "Failed to create sdk for ${module.name} : ${r.error}" }
|
||||
}
|
||||
is Result.Success -> {
|
||||
val sdk = r.result!! // It can't be null: this is an old buggy API that will be fixed soon
|
||||
ModuleRootModificationUtil.setModuleSdk(module, sdk)
|
||||
logger.trace { "SDK creation result for ${module.name} : $sdk" }
|
||||
}
|
||||
}
|
||||
}
|
||||
is CreateSdkInfo.WillCreateEnv -> {
|
||||
logger.trace { "${module.name} can't be configured automatically: no venv for ${info.intentionName}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
is ModuleCreateInfo.SameAs, null -> Unit
|
||||
}
|
||||
}
|
||||
else -> { // Multi-module project, only associate workspace members
|
||||
val modulesToSdk = modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.associateWith { it.suggestSdk() }
|
||||
for ((module, sdkSuggestion) in modulesToSdk) {
|
||||
when (sdkSuggestion) {
|
||||
is SuggestedSdk.PyProjectIndependent, null -> {
|
||||
logger.trace { "${module.name} skipped in multimodule project autoconfig" }
|
||||
}
|
||||
is SuggestedSdk.SameAs -> {
|
||||
val parentSdk = PythonSdkUtil.findPythonSdk(sdkSuggestion.parentModule) ?: continue
|
||||
logger.trace { "${module.name} seeds the same sdk as ${sdkSuggestion.parentModule} : ${parentSdk}" }
|
||||
ModuleRootModificationUtil.setModuleSdk(module, parentSdk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed interface ModuleCreateInfo {
|
||||
data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId, val moduleDir: Directory?) : ModuleCreateInfo
|
||||
data class SameAs(val parentModuleName: ModuleName) : ModuleCreateInfo
|
||||
}
|
||||
|
||||
private val logger = fileLogger()
|
||||
|
||||
/**
|
||||
* For multiple calls, pull [configuratorsByTool] up not to create it each time
|
||||
*/
|
||||
private suspend fun getModuleInfo(
|
||||
module: Module,
|
||||
configuratorsByTool: Map<ToolId, PyProjectSdkConfigurationExtension> = PyProjectSdkConfigurationExtension.createMap(),
|
||||
): ModuleCreateInfo? = // Save on module level
|
||||
when (val r = module.suggestSdk()) {
|
||||
is SuggestedSdk.PyProjectIndependent -> {
|
||||
val tools = r.preferTools.map { configuratorsByTool[it]!! }
|
||||
tools.firstNotNullOfOrNull { tool ->
|
||||
val createInfo = (tool.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(module)
|
||||
?: tool.checkEnvironmentAndPrepareSdkCreator(module)) ?: return@firstNotNullOfOrNull null
|
||||
CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO(r.moduleDir)
|
||||
}
|
||||
}
|
||||
is SuggestedSdk.SameAs -> {
|
||||
ModuleCreateInfo.SameAs(r.parentModule.name)
|
||||
}
|
||||
null -> null
|
||||
} // No tools or not pyproject.toml at all? Use EP as a fallback
|
||||
?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull()
|
||||
?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO(module.guessModuleDir()?.toNioPath()) }
|
||||
|
||||
private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo =
|
||||
ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir)
|
||||
|
||||
@@ -14,28 +14,35 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val askUserMutex = Mutex()
|
||||
private val configureMutex = Mutex()
|
||||
|
||||
/**
|
||||
* Same as [configureSdkAskingUser] but in a separate coroutine
|
||||
* Same as [configureSdk] but in a separate coroutine
|
||||
*/
|
||||
internal fun configureSdkAskingUserBg(project: Project) {
|
||||
internal fun configureSdkBg(project: Project, mode: ModuleConfigurationMode) {
|
||||
project.service<MyService>().scope.launch(Dispatchers.Default) {
|
||||
configureSdkAskingUser(project)
|
||||
configureSdk(project, mode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for list of modules and configure them
|
||||
* Configure SDKs for [project] in [ModuleConfigurationMode] manner (see its doc for the semantics)
|
||||
*/
|
||||
internal suspend fun configureSdkAskingUser(project: Project) {
|
||||
internal suspend fun configureSdk(project: Project, mode: ModuleConfigurationMode) {
|
||||
withContext(Dispatchers.Default) {
|
||||
askUserMutex.withLock {
|
||||
val moduleToSuggestedSdk = ModulesSdkConfigurator.create(project)
|
||||
val modulesDTO = moduleToSuggestedSdk.modulesDTO
|
||||
if (modulesDTO.isNotEmpty()) {
|
||||
// No need to send empty list
|
||||
SHOW_SDK_CONFIG_UI_TOPIC.sendToClient(project, ModulesDTO(modulesDTO))
|
||||
configureMutex.withLock {
|
||||
when (mode) {
|
||||
ModuleConfigurationMode.AUTOMATIC -> {
|
||||
configureSdkAutomatically(project)
|
||||
}
|
||||
ModuleConfigurationMode.INTERACTIVE -> {
|
||||
val moduleToSuggestedSdk = ModulesSdkConfigurator.create(project)
|
||||
val modulesDTO = moduleToSuggestedSdk.modulesDTO
|
||||
if (modulesDTO.isNotEmpty()) {
|
||||
// No need to send empty list
|
||||
SHOW_SDK_CONFIG_UI_TOPIC.sendToClient(project, ModulesDTO(modulesDTO))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -3,8 +3,8 @@ package com.intellij.python.sdkConfigurator.backend.impl.platformBridge
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUserBg
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkBg
|
||||
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
|
||||
|
||||
internal class ConfigureSDKAction : AnAction() {
|
||||
@@ -14,7 +14,7 @@ internal class ConfigureSDKAction : AnAction() {
|
||||
if (!enableSDKAutoConfigurator) {
|
||||
return
|
||||
}
|
||||
configureSdkAskingUserBg(project)
|
||||
configureSdkBg(project, ModuleConfigurationMode.INTERACTIVE)
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
|
||||
+4
-3
@@ -3,10 +3,11 @@ package com.intellij.python.sdkConfigurator.backend.impl.platformBridge
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUserBg
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkBg
|
||||
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
|
||||
|
||||
internal class ModelRebuiltListenerImpl : ModelRebuiltListener {
|
||||
internal class ModelRebuiltListenerImplToConfigureSdk : ModelRebuiltListener {
|
||||
init {
|
||||
if (!enableSDKAutoConfigurator) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
@@ -14,7 +15,7 @@ internal class ModelRebuiltListenerImpl : ModelRebuiltListener {
|
||||
}
|
||||
|
||||
override fun modelRebuilt(project: Project) {
|
||||
configureSdkAskingUserBg(project)
|
||||
configureSdkBg(project, mode = ModuleConfigurationMode.AUTOMATIC)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.intellij.python.sdkConfigurator.backend.impl.rpcBridge
|
||||
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.project.findProject
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.popModulesSDKConfigurator
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
|
||||
import com.intellij.python.sdkConfigurator.backend.impl.configureSdk
|
||||
import com.intellij.python.sdkConfigurator.common.impl.ModuleName
|
||||
import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi
|
||||
|
||||
@@ -13,6 +14,6 @@ internal object SdkConfiguratorApiImpl : SdkConfiguratorBackEndApi {
|
||||
}
|
||||
|
||||
override suspend fun configureAskingUser(projectId: ProjectId) {
|
||||
configureSdkAskingUser(projectId.findProject())
|
||||
configureSdk(projectId.findProject(), mode = ModuleConfigurationMode.INTERACTIVE)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ typealias EnvExists = Boolean
|
||||
sealed interface CreateSdkInfo : Comparable<CreateSdkInfo> {
|
||||
@get:IntentionName
|
||||
val intentionName: String
|
||||
val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>
|
||||
val sdkCreator: suspend (needsConfirmation: NeedsConfirmation) -> PyResult<Sdk?>
|
||||
|
||||
/**
|
||||
* Nullable SDK is only possible when we requested user confirmation but didn't get it. The idea behind this function is to provide
|
||||
@@ -38,12 +38,12 @@ sealed interface CreateSdkInfo : Comparable<CreateSdkInfo> {
|
||||
data class ExistingEnv(
|
||||
val pythonInfo: PythonInfo,
|
||||
override val intentionName: String,
|
||||
override val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>,
|
||||
override val sdkCreator: suspend (needsConfirmation: NeedsConfirmation) -> PyResult<Sdk?>,
|
||||
) : CreateSdkInfo
|
||||
|
||||
data class WillCreateEnv(
|
||||
override val intentionName: String,
|
||||
override val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>,
|
||||
override val sdkCreator: suspend (needsConfirmation: NeedsConfirmation) -> PyResult<Sdk?>,
|
||||
) : CreateSdkInfo
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ sealed interface EnvCheckerResult {
|
||||
// TODO: Make internal after we drop WSL sdk configurator
|
||||
suspend fun prepareSdkCreator(
|
||||
envChecker: suspend (CheckExistence) -> EnvCheckerResult,
|
||||
sdkCreator: (EnvExists) -> (suspend (NeedsConfirmation) -> PyResult<Sdk?>),
|
||||
sdkCreator: (EnvExists) -> (suspend (needsConfirmation: NeedsConfirmation) -> PyResult<Sdk?>),
|
||||
): CreateSdkInfo? {
|
||||
var res = envChecker(true)
|
||||
return when (res) {
|
||||
|
||||
Reference in New Issue
Block a user