PY-794861: Exclude .venv dir in background.

`.venv` dirs are python vens and should never be edited.

(cherry picked from commit 010004d768cb27f9a9cb7e4891f2b1724e695c1e)

GitOrigin-RevId: 54c33f9c914abb913cb74d8f432f951c4c6a9fc3
This commit is contained in:
Ilya.Kazakevich
2026-02-25 01:20:01 +00:00
committed by intellij-monorepo-bot
parent 4243217ad5
commit 853d7e440d
5 changed files with 114 additions and 7 deletions
@@ -4,6 +4,7 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion
import org.jetbrains.annotations.ApiStatus
@@ -18,11 +19,12 @@ private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.mo
*/
@ApiStatus.Internal
suspend fun startAutoImportIfNeeded(project: Project) {
startVenvExclusion(project)
if (enabled) {
project.service<PyProjectAutoImportService>().start()
}
else {
// User disabled "pyproject.toml -> module" convertion (aka project model rebuilding), but we still need to notify listener,
// User disabled "pyproject.toml -> module" conversion (aka project model rebuilding), but we still need to notify listener,
// so they configure SDK
notifyModelRebuilt(project)
}
@@ -0,0 +1,7 @@
package com.intellij.python.pyproject.model.internal
import com.intellij.openapi.components.Service
import kotlinx.coroutines.CoroutineScope
@Service(Service.Level.PROJECT)
internal class PyProjectScopeService(internal val scope: CoroutineScope)
@@ -3,7 +3,6 @@ package com.intellij.python.pyproject.model.internal.autoImportBridge
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectAware
@@ -17,6 +16,7 @@ import com.intellij.openapi.project.Project
import com.intellij.platform.backend.observation.launchTracked
import com.intellij.project.stateStore
import com.intellij.python.pyproject.model.internal.PY_PROJECT_SYSTEM_ID
import com.intellij.python.pyproject.model.internal.PyProjectScopeService
import com.intellij.python.pyproject.model.internal.notifyModelRebuilt
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemNoTomlContent
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemWithTomlContent
@@ -25,7 +25,6 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.messages.Topic
import com.intellij.util.ui.EDT
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@@ -68,7 +67,7 @@ class PyExternalSystemProjectAware private constructor(
}
override fun reloadProject(context: ExternalSystemProjectReloadContext) {
project.service<PyExternalSystemProjectAwareService>().scope.launchTracked {
project.service<PyProjectScopeService>().scope.launchTracked {
reloadProjectImpl()
}
}
@@ -131,7 +130,4 @@ private val PROJECT_AWARE_TOPIC: Topic<ExternalSystemProjectListener> =
Topic(ExternalSystemProjectListener::class.java, Topic.BroadcastDirection.NONE)
@Service(Service.Level.PROJECT)
private class PyExternalSystemProjectAwareService(val scope: CoroutineScope)
private val log = fileLogger()
@@ -0,0 +1,65 @@
package com.intellij.python.pyproject.model.internal.platformBridge
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.FileIndexFacade
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.AsyncFileListener
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.python.pyproject.model.internal.PyProjectScopeService
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.DEFAULT_VIRTUALENV_DIRNAME
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Excludes [DEFAULT_VIRTUALENV_DIRNAME] from [project] as soon as it appears in index, should only be called once
*/
internal fun startVenvExclusion(project: Project) {
VirtualFileManager.getInstance().addAsyncFileListener(project.service<PyProjectScopeService>().scope) { events ->
if (events.none { it is VFileCreateEvent || it is VFileMoveEvent }) {
// No need to check anything if no file created
null
}
else {
object : AsyncFileListener.ChangeApplier {
override fun afterVfsChange() {
excludeEnvs(project)
}
}
}
}
excludeEnvs(project)
}
private fun excludeEnvs(project: Project) {
project.service<PyProjectScopeService>().scope.launch(Dispatchers.Default) {
mutex.withLock {
val dirs = readAction { FilenameIndex.getVirtualFilesByName(DEFAULT_VIRTUALENV_DIRNAME, GlobalSearchScope.allScope(project)) }
for (venvToExclude in dirs) {
val module = readAction { FileIndexFacade.getInstance(project).getModuleForFile(venvToExclude) } ?: continue
val rootManager = ModuleRootManager.getInstance(module)
if (venvToExclude !in rootManager.excludeRoots) {
writeAction {
val model = rootManager.modifiableModel
val currentRoot = model.contentEntries.firstOrNull { root ->
root.file?.let { VfsUtilCore.isAncestor(it, venvToExclude, false) } == true
} ?: return@writeAction
currentRoot.addExcludeFolder(venvToExclude)
model.commit()
}
}
}
}
}
}
private val mutex = Mutex()
@@ -0,0 +1,37 @@
package com.intellij.python.junit5Tests.unit.pyproject
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.testFramework.junit5.fixture.tempPathFixture
import com.intellij.testFramework.utils.vfs.createDirectory
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.delay
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.milliseconds
@TestApplication
internal class PyExcludeVenvTest {
private val tempDirFixture = tempPathFixture()
private val module by projectFixture().moduleFixture(tempDirFixture, addPathToSourceRoot = true)
@Test
fun testExclude(): Unit = timeoutRunBlocking {
val root = VirtualFileManager.getInstance().refreshAndFindFileByNioPath(tempDirFixture.get())!!
val dir1 = writeAction { root.createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) }
startVenvExclusion(module.project)
val dir2 = writeAction { root.createDirectory("fopp").createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) }
while (module.rootManager.excludeRoots.size != 2) {
delay(100.milliseconds)
}
assertThat("Wrong items excluded", module.rootManager.excludeRoots.toList(), containsInAnyOrder(dir1, dir2))
}
}