From 47cea425897f04ca040c2f3283aad1dcd4197722 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 10 Nov 2025 12:50:21 +0100 Subject: [PATCH] IJPL-217691 avoid extra launch - use ProjectActivity scope GitOrigin-RevId: a47b513216df15561c56102e87195a806b657cf9 --- .../project/impl/ProjectManagerImpl.kt | 44 ++++++++------- .../ProjectRootPersistentStateComponent.kt | 8 ++- .../project/impl/ProjectRootsSynchronizer.kt | 56 ++++++++++--------- .../workspaceModel/ProjectRootEntityTest.kt | 2 +- 4 files changed, 61 insertions(+), 49 deletions(-) diff --git a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt index 10a9d3e06ba5..97728ef8de31 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt @@ -692,7 +692,7 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable { if (Registry.`is`("ide.create.project.root.entity") && options.projectRootDir != null) { val root = options.projectRootDir!!.toVirtualFileUrl(project.workspaceModel.getVirtualFileUrlManager()) - project.serviceAsync().projectRootUrls += root.url + project.serviceAsync().addProjectRoot(root.url) // We also register the project root here, so project view will have "files" node immediately. // Otherwise, it might appear too late causing issues like AMPER-4695 @@ -741,7 +741,7 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable { result?.let { project -> try { withContext(Dispatchers.EDT) { - closeProject(project, saveProject = false, checkCanClose = false) + closeProject(project = project, saveProject = false, checkCanClose = false) } } catch (secondException: Throwable) { @@ -784,16 +784,18 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable { protected open fun createFrameAllocator(projectStoreBaseDir: Path, options: OpenProjectTask): ProjectFrameAllocator { val app = ApplicationManager.getApplication() - return if (app.isHeadlessEnvironment || app.isUnitTestMode) { - HeadlessProjectFrameAllocator() + if (app.isHeadlessEnvironment || app.isUnitTestMode) { + return HeadlessProjectFrameAllocator() } else { - IdeProjectFrameAllocator(options, projectStoreBaseDir) + return IdeProjectFrameAllocator(options, projectStoreBaseDir) } } private suspend fun cancelProjectOpening(project: Project?, e: CancellationException? = null) { - if (project == null) return + if (project == null) { + return + } try { try { @@ -806,7 +808,7 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable { withContext(Dispatchers.EDT) { writeIntentReadAction { - closeProject(project, saveProject = false, checkCanClose = false) + closeProject(project = project, saveProject = false, checkCanClose = false) } } } @@ -847,30 +849,30 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable { } override fun newProject(file: Path, options: OpenProjectTask): Project? { - return try { + try { @Suppress("DEPRECATION") - runUnderModalProgressIfIsEdt { + return runUnderModalProgressIfIsEdt { newProjectAsync(file, options) } } - catch (t: Throwable) { - handleErrorOnNewProject(t) - null + catch (e: Throwable) { + handleErrorOnNewProject(e) + return null } } final override suspend fun newProjectAsync(file: Path, options: OpenProjectTask): Project { - TrustedProjects.setProjectTrusted(file, true) + TrustedProjects.setProjectTrusted(path = file, isTrusted = true) return prepareNewProject( - file, - options.projectName, - options.beforeInit, - options.useDefaultProjectAsTemplate, - options.preloadServices, - options.isProjectCreatedWithWizard, - markAsNew = false + identityFle = file, + projectName = options.projectName, + beforeInit = options.beforeInit, + useDefaultProjectAsTemplate = options.useDefaultProjectAsTemplate, + preloadServices = options.preloadServices, + markAsNewlyCreated = options.isProjectCreatedWithWizard, + markAsNew = false, ).also { project -> - TrustedProjects.setProjectTrusted(project, true) + TrustedProjects.setProjectTrusted(project = project, isTrusted = true) } } diff --git a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootPersistentStateComponent.kt b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootPersistentStateComponent.kt index 5dafb5eff0ac..9382c1313cb1 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootPersistentStateComponent.kt +++ b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootPersistentStateComponent.kt @@ -10,8 +10,12 @@ import org.jetbrains.annotations.ApiStatus @Service(Service.Level.PROJECT) @State(name = "ProjectRoots", storages = [Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE)]) @ApiStatus.Internal -class ProjectRootPersistentStateComponent(val scope: CoroutineScope) : - SerializablePersistentStateComponent(State()) { +class ProjectRootPersistentStateComponent : SerializablePersistentStateComponent(State()) { + fun addProjectRoot(projectRootUrl: String) { + updateState { + it.copy(projectRootUrls = it.projectRootUrls + projectRootUrl) + } + } var projectRootUrls: List get() = state.projectRootUrls diff --git a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootsSynchronizer.kt b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootsSynchronizer.kt index 426b4c369251..d8f2ab1992ae 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootsSynchronizer.kt +++ b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectRootsSynchronizer.kt @@ -1,7 +1,9 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.project.impl +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.serviceAsync +import com.intellij.openapi.extensions.ExtensionNotApplicableException import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.util.registry.Registry @@ -9,44 +11,48 @@ import com.intellij.platform.backend.workspace.WorkspaceModel import com.intellij.platform.workspace.storage.entities import com.intellij.workspaceModel.ide.ProjectRootEntity import com.intellij.workspaceModel.ide.registerProjectRoot -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.ApiStatus.Internal +import org.jetbrains.annotations.VisibleForTesting -@ApiStatus.Internal +@Internal class ProjectRootsSynchronizer : ProjectActivity { - override suspend fun execute(project: Project) { - if (!Registry.`is`("ide.create.project.root.entity")) return + companion object { + @VisibleForTesting + suspend fun doRegister(project: Project) { + if (!Registry.`is`("ide.create.project.root.entity")) { + return + } - val projectRootsComponent = project.serviceAsync() - val roots = projectRootsComponent.projectRootUrls - val virtualFileUrlManager = project.serviceAsync().getVirtualFileUrlManager() - for (root in roots) { - registerProjectRoot(project, virtualFileUrlManager.getOrCreateFromUrl(root)) + val projectRootsComponent = project.serviceAsync() + val roots = projectRootsComponent.projectRootUrls + val virtualFileUrlManager = project.serviceAsync().getVirtualFileUrlManager() + for (root in roots) { + registerProjectRoot(project, virtualFileUrlManager.getOrCreateFromUrl(root)) + } } + } - launchListener(project, projectRootsComponent.scope) + override suspend fun execute(project: Project) { + doRegister(project) + return + + launchListener(project) } /** * Keeps [ProjectRootPersistentStateComponent] in sync with the actual ProjectRootEntities in the workspace model. */ - private fun launchListener(project: Project, scope: CoroutineScope) { + private suspend fun launchListener(project: Project) { + val workspaceModel = project.serviceAsync() + val flow = workspaceModel.eventLog + .filter { change -> change.getChanges(ProjectRootEntity::class.java).isNotEmpty() } - scope.launch { - val workspaceModel = project.serviceAsync() - val flow = workspaceModel.eventLog - .filter { change -> change.getChanges(ProjectRootEntity::class.java).isNotEmpty() } - - val component = project.serviceAsync() - component.projectRootUrls = workspaceModel.currentSnapshot.entities().map { it.root.url }.toList() - - flow.collect { change -> - val newRoots = change.storageAfter.entities().map { it.root.url }.toList() - val component = project.serviceAsync() - component.projectRootUrls = newRoots.toList() - } + val component = project.serviceAsync() + component.projectRootUrls = workspaceModel.currentSnapshot.entities().map { it.root.url }.toList() + flow.collect { change -> + component.projectRootUrls = change.storageAfter.entities().map { it.root.url }.toList() } } } diff --git a/platform/platform-tests/testSrc/com/intellij/workspaceModel/ProjectRootEntityTest.kt b/platform/platform-tests/testSrc/com/intellij/workspaceModel/ProjectRootEntityTest.kt index 3f3eddf831c4..b0ddfa42658d 100644 --- a/platform/platform-tests/testSrc/com/intellij/workspaceModel/ProjectRootEntityTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/workspaceModel/ProjectRootEntityTest.kt @@ -31,7 +31,7 @@ class ProjectRootEntityTest { projectFile.createDirectories() val options = createTestOpenProjectOptions().copy(projectName = name, projectRootDir = projectFile) ProjectUtil.openOrImportAsync(projectFile, options)!!.useProjectAsync { project -> - ProjectRootsSynchronizer().execute(project) // background startup activities are not executed in unit tests on project open + ProjectRootsSynchronizer.doRegister(project) // background startup activities are not executed in unit tests on project open val roots = project.workspaceModel.currentSnapshot.entities(ProjectRootEntity::class.java).toList() assertThat(roots.map { it.root }).isEqualTo(listOf(projectFile.toVirtualFileUrl(project.workspaceModel.getVirtualFileUrlManager()))) }