mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-87445: Obey PyProject enable/disable feature.
`PyProjectAutoImportService` is only automatically started by `startAutoImportIfNeeded` if `PyProjectModelSettings.getUsePyprojectToml` is enabled. Once enabled/disabled, `PyProjectModelSettings` starts/stops `PyProjectAutoImportService`. Also: 1. Move `PyProjectModelStartupActivity` to `askUserIfPyProjectMustBeEnabled` to make sure it is called __after__ project creation. Otherwise, it might start project import process in the middle of project creation and delete modules. 2. Make import non-suspend (no need to access IO anyway) to make sure we can call it from `PyProjectModelSettings` GitOrigin-RevId: e9328721ad90c76d1a473cffa15c7fec2e365c9c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
52d2828b6d
commit
46cda23bf1
-58
@@ -10,36 +10,17 @@ import com.intellij.openapi.options.SearchableConfigurable
|
||||
import com.intellij.openapi.roots.ModifiableRootModel
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
import com.intellij.openapi.roots.ui.configuration.DefaultModulesProvider
|
||||
import com.intellij.openapi.ui.Splitter
|
||||
import com.intellij.python.pyproject.model.PyProjectModelSettings
|
||||
import com.intellij.ui.AncestorListenerAdapter
|
||||
import com.intellij.ui.roots.IconActionComponent
|
||||
import com.intellij.ui.roots.ToolbarPanel
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.module.PyContentEntriesEditor
|
||||
import org.jetbrains.jps.model.java.JavaSourceRootType
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.JTree
|
||||
import javax.swing.event.AncestorEvent
|
||||
|
||||
internal class PyContentEntriesModuleConfigurable(private val module: Module) : SearchableConfigurable.Parent.Abstract() {
|
||||
|
||||
private val topPanel = JPanel(BorderLayout())
|
||||
//.apply {
|
||||
// addAncestorListener(object : AncestorListenerAdapter() {
|
||||
// override fun ancestorAdded(event: AncestorEvent) {
|
||||
// updateEnabledState()
|
||||
// }
|
||||
// })
|
||||
//}
|
||||
|
||||
private var modifiableModel: ModifiableRootModel? = null
|
||||
private var editor: PyContentEntriesEditor? = null
|
||||
private val isPyProjectTomlManaged: Boolean
|
||||
@@ -87,7 +68,6 @@ internal class PyContentEntriesModuleConfigurable(private val module: Module) :
|
||||
ApplicationManager.getApplication().runWriteAction { modifiableModel?.commit() }
|
||||
resetEditor()
|
||||
}
|
||||
//updateEnabledState()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
@@ -104,38 +84,6 @@ internal class PyContentEntriesModuleConfigurable(private val module: Module) :
|
||||
createEditor()
|
||||
}
|
||||
|
||||
private fun updateEnabledState() {
|
||||
val visible = !isPyProjectTomlManaged
|
||||
|
||||
UIUtil.findComponentOfType(topPanel, Splitter::class.java)?.let { splitter ->
|
||||
// Hide the "Mark as" label and action toolbar (siblings of the splitter in contentPanel)
|
||||
splitter.parent?.let { contentPanel ->
|
||||
contentPanel.components.filter { it != splitter }.forEach { it.isVisible = visible }
|
||||
}
|
||||
|
||||
// In the right panel (ToolbarPanel): hide the "Add Content Root" toolbar and remove-root icons
|
||||
val contentRootsPanel = splitter.secondComponent
|
||||
(contentRootsPanel as? ToolbarPanel)?.let {
|
||||
it.getComponent(0).isVisible = visible
|
||||
}
|
||||
UIUtil.findComponentsOfType(contentRootsPanel, IconActionComponent::class.java).forEach {
|
||||
it.isVisible = visible
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress tree context menu without removing PopupHandler listeners
|
||||
UIUtil.findComponentOfType(topPanel, JTree::class.java)?.let { tree ->
|
||||
tree.componentPopupMenu = if (visible) null else EMPTY_POPUP
|
||||
}
|
||||
|
||||
// Hide "Exclude patterns" field, its label, and comment (all are siblings of the tree JScrollPane)
|
||||
UIUtil.findComponentOfType(topPanel, JTextField::class.java)?.let { excludeField ->
|
||||
excludeField.parent?.let { dialogPanel ->
|
||||
dialogPanel.components.filter { it !is JScrollPane }.forEach { it.isVisible = visible }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disposeUIResources() {
|
||||
editor?.let {
|
||||
it.disposeUIResources()
|
||||
@@ -149,10 +97,4 @@ internal class PyContentEntriesModuleConfigurable(private val module: Module) :
|
||||
override fun buildConfigurables(): Array<Configurable> = emptyArray()
|
||||
|
||||
override fun getId(): String = "python.project.structure"
|
||||
|
||||
companion object {
|
||||
private val EMPTY_POPUP = object : JPopupMenu() {
|
||||
override fun show(invoker: Component?, x: Int, y: Int) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@Internal
|
||||
package com.intellij.pycharm.community.ide.impl.configuration;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus.Internal;
|
||||
@@ -14,7 +14,6 @@
|
||||
<notificationGroup id="PyProject.toml" displayType="STICKY_BALLOON" isLogByDefault="true"
|
||||
bundle="messages.PyProjectTomlBundle" key="pyproject.notification.title"/>
|
||||
<backgroundPostStartupActivity implementation="com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity"/>
|
||||
<backgroundPostStartupActivity implementation="com.intellij.python.pyproject.model.internal.PyProjectModelStartupActivity"/>
|
||||
|
||||
<statistics.projectUsagesCollector implementation="com.intellij.python.pyproject.statistics.PythonTomlStatsUsagesCollector"/>
|
||||
</extensions>
|
||||
|
||||
+16
-3
@@ -10,11 +10,13 @@ import com.intellij.openapi.components.Storage
|
||||
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
|
||||
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
@State(name = "PyProjectModelSettings", storages = [Storage("pyProjectModel.xml")])
|
||||
class PyProjectModelSettings : PersistentStateComponent<PyProjectModelSettings.State>, Disposable {
|
||||
class PyProjectModelSettings(private val project: Project) :
|
||||
PersistentStateComponent<PyProjectModelSettings.State>, Disposable {
|
||||
override fun dispose() {}
|
||||
class State : BaseState() {
|
||||
var usePyprojectToml: Boolean by property(false)
|
||||
@@ -23,14 +25,20 @@ class PyProjectModelSettings : PersistentStateComponent<PyProjectModelSettings.S
|
||||
|
||||
private var myState = State()
|
||||
|
||||
var onChanged: Runnable? = null // TODO trigger logic here
|
||||
|
||||
var usePyprojectToml: Boolean
|
||||
get() = isFeatureEnabled && myState.usePyprojectToml
|
||||
set(value) {
|
||||
if (myState.usePyprojectToml != value) {
|
||||
myState.usePyprojectToml = value
|
||||
onChanged?.run()
|
||||
project.service<PyProjectAutoImportService>().apply {
|
||||
if (value) {
|
||||
start()
|
||||
}
|
||||
else {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +58,11 @@ class PyProjectModelSettings : PersistentStateComponent<PyProjectModelSettings.S
|
||||
@JvmStatic
|
||||
fun getInstance(project: Project): PyProjectModelSettings = project.service()
|
||||
|
||||
/**
|
||||
* Hard setting: if disabled -> feature is disabled on the Registry.
|
||||
* For user-defined setting, check [PyProjectModelSettings.usePyprojectToml].
|
||||
* Be sure to check **both** except for UI for the aforementioned service.
|
||||
*/
|
||||
val isFeatureEnabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -9,16 +9,18 @@ import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
|
||||
/**
|
||||
* Starts autoimport process if [enabled] or simply "skips" to the next step: [notifyModelRebuilt].
|
||||
* Starts autoimport process if [PyProjectModelSettings.isFeatureEnabled] or simply "skips" to the next step: [notifyModelRebuilt].
|
||||
* This method usually called by [com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity] except for new projects.
|
||||
* In this case, it is postponed till project generation (see usages).
|
||||
*
|
||||
* This method can only be called once (see [PyProjectAutoImportService.start])
|
||||
* This method can only be called **once** (call [PyProjectAutoImportService.start] to enable/disable import logic)
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun startAutoImportIfNeeded(project: Project) {
|
||||
startVenvExclusion(project)
|
||||
if (PyProjectModelSettings.isFeatureEnabled) {
|
||||
askUserIfPyProjectMustBeEnabled(project)
|
||||
// Only start autoImport if both: registry and user flags are enabled
|
||||
if (PyProjectModelSettings.isFeatureEnabled && PyProjectModelSettings.getInstance(project).usePyprojectToml) {
|
||||
project.service<PyProjectAutoImportService>().start()
|
||||
}
|
||||
else {
|
||||
|
||||
+37
-40
@@ -8,7 +8,6 @@ import com.intellij.notification.impl.NotificationFullContent
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.psi.search.FilenameIndex
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
@@ -18,50 +17,48 @@ import org.jetbrains.annotations.Nls
|
||||
|
||||
private const val NOTIFICATION_GROUP_ID = "PyProject.toml"
|
||||
|
||||
internal class PyProjectModelStartupActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
if (!PyProjectModelSettings.isFeatureEnabled) return
|
||||
internal suspend fun askUserIfPyProjectMustBeEnabled(project: Project) {
|
||||
if (!PyProjectModelSettings.isFeatureEnabled) return
|
||||
|
||||
val settings = PyProjectModelSettings.getInstance(project)
|
||||
if (!settings.showConfigurationNotification) return
|
||||
val settings = PyProjectModelSettings.getInstance(project)
|
||||
if (!settings.showConfigurationNotification) return
|
||||
|
||||
val hasPyprojectToml = readAction {
|
||||
!project.isDisposed && FilenameIndex.getVirtualFilesByName(PY_PROJECT_TOML, GlobalSearchScope.projectScope(project)).isNotEmpty()
|
||||
}
|
||||
|
||||
if (hasPyprojectToml) {
|
||||
showNotification(project, settings)
|
||||
}
|
||||
else {
|
||||
listenForPyprojectToml(project, settings)
|
||||
}
|
||||
val hasPyprojectToml = readAction {
|
||||
!project.isDisposed && FilenameIndex.getVirtualFilesByName(PY_PROJECT_TOML, GlobalSearchScope.projectScope(project)).isNotEmpty()
|
||||
}
|
||||
|
||||
private fun listenForPyprojectToml(project: Project, settings: PyProjectModelSettings) {
|
||||
val disposable = Disposer.newDisposable("PyProjectModelStartupActivity")
|
||||
Disposer.register(settings, disposable)
|
||||
if (hasPyprojectToml) {
|
||||
showNotification(project, settings)
|
||||
}
|
||||
else {
|
||||
listenForPyprojectToml(project, settings)
|
||||
}
|
||||
}
|
||||
|
||||
project.messageBus.connect(disposable).subscribe(DumbService.DUMB_MODE, object : DumbService.DumbModeListener {
|
||||
override fun exitDumbMode() {
|
||||
if (!settings.showConfigurationNotification) {
|
||||
Disposer.dispose(disposable)
|
||||
return
|
||||
}
|
||||
private fun listenForPyprojectToml(project: Project, settings: PyProjectModelSettings) {
|
||||
val disposable = Disposer.newDisposable("PyProjectModelStartupActivity")
|
||||
Disposer.register(settings, disposable)
|
||||
|
||||
val hasAnyPyprojectToml = FilenameIndex.hasVirtualFileWithName(
|
||||
PY_PROJECT_TOML,
|
||||
true,
|
||||
GlobalSearchScope.projectScope(project),
|
||||
null
|
||||
)
|
||||
|
||||
if (hasAnyPyprojectToml) {
|
||||
Disposer.dispose(disposable)
|
||||
showNotification(project, settings)
|
||||
}
|
||||
project.messageBus.connect(disposable).subscribe(DumbService.DUMB_MODE, object : DumbService.DumbModeListener {
|
||||
override fun exitDumbMode() {
|
||||
if (!settings.showConfigurationNotification) {
|
||||
Disposer.dispose(disposable)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val hasAnyPyprojectToml = FilenameIndex.hasVirtualFileWithName(
|
||||
PY_PROJECT_TOML,
|
||||
true,
|
||||
GlobalSearchScope.projectScope(project),
|
||||
null
|
||||
)
|
||||
|
||||
if (hasAnyPyprojectToml) {
|
||||
Disposer.dispose(disposable)
|
||||
showNotification(project, settings)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showNotification(project: Project, settings: PyProjectModelSettings) {
|
||||
@@ -82,5 +79,5 @@ private fun showNotification(project: Project, settings: PyProjectModelSettings)
|
||||
.notify(project)
|
||||
}
|
||||
|
||||
private class FullContentNotification(groupId: String, @Nls title: String, @Nls content: String, type: NotificationType)
|
||||
: Notification(groupId, title, content, type), NotificationFullContent
|
||||
private class FullContentNotification(groupId: String, @Nls title: String, @Nls content: String, type: NotificationType) :
|
||||
Notification(groupId, title, content, type), NotificationFullContent
|
||||
|
||||
+3
-7
@@ -25,8 +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.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.nio.file.Path
|
||||
@@ -113,12 +111,10 @@ class PyExternalSystemProjectAware private constructor(
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@VisibleForTesting
|
||||
suspend fun create(project: Project): PyExternalSystemProjectAware {
|
||||
fun create(project: Project): PyExternalSystemProjectAware {
|
||||
assert(!project.isDefault) { "Default project not supported" }
|
||||
val baseDir = withContext(Dispatchers.IO) {
|
||||
// guessPath doesn't work: it returns first module path
|
||||
project.stateStore.projectBasePath
|
||||
}
|
||||
// guessPath doesn't work: it returns first module path
|
||||
val baseDir = project.stateStore.projectBasePath
|
||||
return PyExternalSystemProjectAware(project, baseDir)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-17
@@ -2,44 +2,58 @@ package com.intellij.python.pyproject.model.internal.autoImportBridge
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
|
||||
import com.intellij.openapi.project.Project
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
|
||||
/**
|
||||
* [project] can't be default, check for it
|
||||
*/
|
||||
@Service(Service.Level.PROJECT)
|
||||
internal class PyProjectAutoImportService(private val project: Project) : Disposable {
|
||||
@ApiStatus.Internal
|
||||
class PyProjectAutoImportService(private val project: Project) : Disposable {
|
||||
private val m = Any()
|
||||
|
||||
init {
|
||||
assert(!project.isDefault) { "Default project not supported" }
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var projectId: ExternalSystemProjectId? = null
|
||||
|
||||
@get:TestOnly
|
||||
internal val initialized: Boolean get() = projectId != null
|
||||
internal val initialized: Boolean get() = synchronized(m) { projectId != null }
|
||||
|
||||
|
||||
/**
|
||||
* Starts auto-import (`builds project module on any pyproject.toml` change). To be called only once!
|
||||
* Starts auto-import (`builds project module on any pyproject.toml` change). Does nothing if already started. Method is synchronized.
|
||||
* You can always [stop] it, so does [dispose]
|
||||
*/
|
||||
internal suspend fun start() {
|
||||
assert(projectId == null) { "Already started, do not call second time" }
|
||||
val tracker = getTracker()
|
||||
val projectAware = PyExternalSystemProjectAware.create(project)
|
||||
val projectId = projectAware.projectId
|
||||
this.projectId = projectId
|
||||
tracker.register(projectAware)
|
||||
tracker.activate(projectId)
|
||||
tracker.markDirty(projectId)
|
||||
tracker.scheduleProjectRefresh()
|
||||
fun start(): Unit = synchronized(m) {
|
||||
if (projectId != null) {
|
||||
log.info("PyProject already started")
|
||||
return@synchronized
|
||||
}
|
||||
else {
|
||||
val tracker = getTracker()
|
||||
val projectAware = PyExternalSystemProjectAware.create(project)
|
||||
val projectId = projectAware.projectId
|
||||
this.projectId = projectId
|
||||
tracker.register(projectAware)
|
||||
tracker.activate(projectId)
|
||||
tracker.markDirty(projectId)
|
||||
tracker.scheduleProjectRefresh()
|
||||
log.info("PyProject started")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
/**
|
||||
* Stop auto-import (started by [start]) does nothing if already stopped. Method is synchronized.
|
||||
*/
|
||||
fun stop(): Unit = synchronized(m) {
|
||||
log.info("PyProject stopped")
|
||||
projectId?.let {
|
||||
getTracker().remove(it)
|
||||
projectId = null
|
||||
@@ -47,5 +61,14 @@ internal class PyProjectAutoImportService(private val project: Project) : Dispos
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
stop()
|
||||
}
|
||||
|
||||
|
||||
private fun getTracker(): ExternalSystemProjectTracker = ExternalSystemProjectTracker.getInstance(project)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val log = fileLogger()
|
||||
}
|
||||
}
|
||||
|
||||
+30
-5
@@ -2,13 +2,17 @@ package com.intellij.python.junit5Tests.unit.pyproject
|
||||
|
||||
import com.intellij.ide.impl.OpenProjectTask
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.python.pyproject.model.PyProjectModelSettings
|
||||
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
|
||||
import com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity
|
||||
import com.intellij.python.pyproject.model.internal.startAutoImportIfNeeded
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
|
||||
@TestApplication
|
||||
internal class PyProjectSyncActivityTest {
|
||||
@@ -16,9 +20,30 @@ internal class PyProjectSyncActivityTest {
|
||||
projectFixture(openProjectTask = OpenProjectTask().copy(isProjectCreatedWithWizard = true), openAfterCreation = true)
|
||||
|
||||
|
||||
@Test
|
||||
fun testNoAutoRebuildForWizardBasedProject(): Unit = timeoutRunBlocking {
|
||||
PyProjectSyncActivity().execute(project)
|
||||
assertFalse(project.service<PyProjectAutoImportService>().initialized, "Newly opened project shouldn't lead to autoimport ")
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = [true, false])
|
||||
fun testNoAutoRebuildForWizardBasedProject(enableAutoImport: Boolean): Unit = timeoutRunBlocking {
|
||||
val settings = project.service<PyProjectModelSettings>()
|
||||
val oldValue = settings.usePyprojectToml
|
||||
settings.usePyprojectToml = enableAutoImport
|
||||
// Setting this var starts import automatically, so we stop it check that code starts it in tests
|
||||
project.service<PyProjectAutoImportService>().stop()
|
||||
try {
|
||||
val sut = project.service<PyProjectAutoImportService>()
|
||||
PyProjectSyncActivity().execute(project)
|
||||
assertFalse(sut.initialized, "Newly opened project shouldn't lead to autoimport ")
|
||||
startAutoImportIfNeeded(project)
|
||||
if (enableAutoImport) {
|
||||
assertTrue(sut.initialized, "Auto import must be started when called manually")
|
||||
}
|
||||
else {
|
||||
assertFalse(sut.initialized, "Auto import started, even though was disabled")
|
||||
}
|
||||
sut.stop()
|
||||
assertFalse(sut.initialized, "Autoimport must be stopped")
|
||||
}
|
||||
finally {
|
||||
settings.usePyprojectToml = oldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user