diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPane.java b/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPane.java index 63801363bf8e..870219b11c67 100644 --- a/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPane.java +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPane.java @@ -34,7 +34,6 @@ import com.intellij.psi.*; import com.intellij.psi.util.PsiAwareObject; import com.intellij.psi.util.PsiUtilCore; import com.intellij.refactoring.move.MoveHandler; -import com.intellij.ui.tree.AsyncTreeModel; import com.intellij.ui.tree.TreePathUtil; import com.intellij.ui.tree.TreeVisitor; import com.intellij.ui.tree.project.ProjectFileNode; diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPaneWithAsyncSupport.java b/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPaneWithAsyncSupport.java index 59229d21956b..f39c53634e05 100644 --- a/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPaneWithAsyncSupport.java +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/AbstractProjectViewPaneWithAsyncSupport.java @@ -89,7 +89,8 @@ public abstract class AbstractProjectViewPaneWithAsyncSupport extends AbstractPr }); } myTreeStructure = createStructure(); - myAsyncSupport = new AsyncProjectViewSupport(this, myProject, myTreeStructure, createComparator()); + myAsyncSupport = myProject.getService(ProjectViewPaneSupportService.class) + .createProjectViewPaneSupport(this, myTreeStructure, createComparator()); configureAsyncSupport(myAsyncSupport); myAsyncSupport.setModelTo(myTree); @@ -242,6 +243,7 @@ public abstract class AbstractProjectViewPaneWithAsyncSupport extends AbstractPr @ApiStatus.Internal public @NotNull AsyncProjectViewSupport createAsyncSupport(@NotNull Disposable parent, @NotNull Comparator> comparator) { + // TODO: how do we apply the new implementation to CWM? return new AsyncProjectViewSupport(parent, myProject, createStructure(), comparator); } } diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/CoroutineProjectViewSupport.kt b/platform/lang-impl/src/com/intellij/ide/projectView/impl/CoroutineProjectViewSupport.kt new file mode 100644 index 000000000000..9421650e1f81 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/CoroutineProjectViewSupport.kt @@ -0,0 +1,201 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ide.projectView.impl + +import com.intellij.ide.util.treeView.AbstractTreeNode +import com.intellij.ide.util.treeView.AbstractTreeStructure +import com.intellij.ide.util.treeView.NodeDescriptor +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readAction +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.debug +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.ActionCallback +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.ui.tree.RestoreSelectionListener +import com.intellij.ui.tree.TreeCollector +import com.intellij.ui.tree.TreeVisitor +import com.intellij.ui.tree.project.ProjectFileNode.findArea +import com.intellij.ui.tree.project.ProjectFileNodeUpdater +import com.intellij.ui.treeStructure.TreeDomainModel +import com.intellij.ui.treeStructure.TreeSwingModel +import com.intellij.ui.treeStructure.TreeViewModel +import com.intellij.util.SmartList +import kotlinx.coroutines.* +import javax.swing.JTree +import javax.swing.SwingUtilities +import javax.swing.tree.TreePath + +internal class CoroutineProjectViewSupport( + pane: AbstractProjectViewPaneWithAsyncSupport, + private val project: Project, + private val coroutineScope: CoroutineScope, + treeStructure: AbstractTreeStructure, +) : ProjectViewPaneSupport() { + + private val domainModel = TreeDomainModel(treeStructure, true, 1) + private val viewModel = TreeViewModel(coroutineScope, domainModel) + private val swingModel = TreeSwingModel(coroutineScope, viewModel) + + init { + Disposer.register(pane, Disposable { + coroutineScope.cancel() + }) + myNodeUpdater = Updater(project, coroutineScope) + setupListeners(pane, project, treeStructure) + } + + override fun setModelTo(tree: JTree) { + val restoreSelectionListener = RestoreSelectionListener() + tree.addTreeSelectionListener(restoreSelectionListener) + tree.model = swingModel + coroutineScope.coroutineContext.job.invokeOnCompletion { + SwingUtilities.invokeLater { + tree.removeTreeSelectionListener(restoreSelectionListener) + } + } + } + + override fun setComparator(comparator: Comparator>?) { + if (comparator == null) { + viewModel.comparator = null + return + } + viewModel.comparator = Comparator.comparing( + { viewModel -> viewModel.getUserObject() as? NodeDescriptor<*> }, + comparator::compare, + ) + } + + override fun updateAll(afterUpdate: Runnable?) { + updateImpl(null, true, afterUpdate) + } + + override fun update(path: TreePath, updateStructure: Boolean) { + updateImpl(path, updateStructure) + } + + private fun updateImpl(element: TreePath?, updateStructure: Boolean, onDone: Runnable? = null) { + val job = coroutineScope.launch(CoroutineName("Updating $element, structure=$updateStructure")) { + viewModel.invalidate(element, updateStructure) + } + job.invokeOnCompletion { + onDone?.let { SwingUtilities.invokeLater(it) } + } + } + + override fun acceptAndUpdate(visitor: TreeVisitor, presentations: List?, structures: List?) { + coroutineScope.launch(CoroutineName("Updating ${presentations?.size} presentations and ${structures?.size} structures after accepting $visitor")) { + viewModel.accept(visitor, false) + } + } + + override fun select(tree: JTree, toSelect: Any?, file: VirtualFile?): ActionCallback { + selectLogger.debug { "CoroutineProjectViewSupport.select: object=$toSelect, file=$file" } + val value = if (toSelect is AbstractTreeNode<*>) { + toSelect.value?.also { retrieved -> + selectLogger.debug { "Retrieved the value from the node: $retrieved" } + } + } + else { + toSelect + } + val element = toSelect as? PsiElement + selectLogger.debug { "select object: $value in file: $file" } + val callback = ActionCallback() + selectLogger.debug("Updating nodes before selecting") + myNodeUpdater.updateImmediately { + selectLogger.debug("Updated nodes") + val job = coroutineScope.launch(CoroutineName("Selecting $value in $file") + Dispatchers.EDT) { + thisLogger().debug("First attempt: trying to select the element or file") + if (trySelect(tree, element, file)) { + thisLogger().debug("Selected paths at first attempt. Done") + return@launch + } + if (!canTrySelectAgain(element, file)) return@launch + // This silly second attempt is necessary because a file, when visited, may tell us it doesn't contain the element we're looking for. + // Reportedly, it's the case with top-level Kotlin functions and Kotlin files. + thisLogger().debug("Second attempt: trying to select the file now") + if (trySelect(tree, null, file)) { + thisLogger().debug("Selected successfully at the second attempt. Done") + } + else { + thisLogger().debug("Couldn't select at the second attempt. Done") + } + } + job.invokeOnCompletion { + callback.setDone() + } + } + return callback + } + + private suspend fun trySelect(tree: JTree, element: PsiElement?, file: VirtualFile?): Boolean { + val pathsToSelect = SmartList() + val visitor = AbstractProjectViewPane.createVisitor(element, file, pathsToSelect) + if (visitor == null) { + selectLogger.debug("We don't have neither a valid element nor a file. Done") + return false + } + selectLogger.debug("Collecting paths to select") + viewModel.accept(visitor, true) + selectLogger.debug { "Collected paths to select: $pathsToSelect" } + return selectPaths(tree, pathsToSelect, visitor) + } + + private fun canTrySelectAgain(element: PsiElement?, file: VirtualFile?): Boolean { + if (element == null) { + selectLogger.debug( + "Couldn't select paths at first attempt, " + + "but a second attempt isn't possible because the given element is null " + + "and therefore we have already tried looking for the file during the first attempt. Done" + ) + return false + } + if (file == null) { + selectLogger.debug( + "Couldn't select paths at first attempt, " + + "but a second attempt isn't possible because the given file is null. Done" + ) + return false + } + if (Registry.`is`("async.project.view.support.extra.select.disabled", false)) { + selectLogger.debug( + "Couldn't select paths at first attempt, " + + "but a second attempt isn't possible because it's disabled in the Registry. Done" + ) + return false + } + return true + } + + private inner class Updater(project: Project, coroutineScope: CoroutineScope) : ProjectFileNodeUpdater(project, coroutineScope) { + override fun updateStructure(fromRoot: Boolean, updatedFiles: Set) { + if (fromRoot) { + updateAll(null) + return + } + coroutineScope.launch(CoroutineName("Updating ${updatedFiles.size} files")) { + val roots = readAction { + val collector = TreeCollector.VirtualFileRoots.create() + for (file in updatedFiles) { + val dir = if (file.isDirectory) file else file.parent + if (dir != null && findArea(dir, project) != null) collector.add(file) + } + collector.get() + } + for (root in roots) { + updateByFile(root, true) + } + } + } + } +} + +private val CoroutineProjectViewSupport.selectLogger: Logger + get() = logger() diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewPaneSupportService.kt b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewPaneSupportService.kt new file mode 100644 index 000000000000..69eab146cd85 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewPaneSupportService.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ide.projectView.impl + +import com.intellij.ide.util.treeView.AbstractTreeStructure +import com.intellij.ide.util.treeView.NodeDescriptor +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.platform.util.coroutines.childScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel + +@Service(Service.Level.PROJECT) +internal class ProjectViewPaneSupportService( + private val project: Project, + private val coroutineScope: CoroutineScope, +) { + fun createProjectViewPaneSupport( + pane: AbstractProjectViewPaneWithAsyncSupport, + treeStructure: AbstractTreeStructure, + comparator: java.util.Comparator>, + ): ProjectViewPaneSupport = + if (Registry.`is`("ide.project.view.coroutines", false)) { + val scope = coroutineScope.childScope("ProjectViewPaneSupport id=${pane.id}, subId=${pane.subId}") + Disposer.register(pane, Disposable { scope.cancel() }) + CoroutineProjectViewSupport(pane, project, scope, treeStructure) + } + else { + AsyncProjectViewSupport(pane, project, treeStructure, comparator) + } +} diff --git a/platform/platform-impl/api-dump-unreviewed.txt b/platform/platform-impl/api-dump-unreviewed.txt index 8e827e838826..18f9ef806b40 100644 --- a/platform/platform-impl/api-dump-unreviewed.txt +++ b/platform/platform-impl/api-dump-unreviewed.txt @@ -25660,6 +25660,7 @@ com.intellij.ui.tree.project.ProjectFileNode - a:getVirtualFile():com.intellij.openapi.vfs.VirtualFile a:com.intellij.ui.tree.project.ProjectFileNodeUpdater - (com.intellij.openapi.project.Project,com.intellij.util.concurrency.Invoker):V +- (com.intellij.openapi.project.Project,kotlinx.coroutines.CoroutineScope):V - p:getUpdatingDelay():I - updateFromElement(com.intellij.psi.PsiElement):V - updateFromFile(com.intellij.openapi.vfs.VirtualFile):V diff --git a/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdater.java b/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdater.java index a62ae7cd067a..f6925e75f7c8 100644 --- a/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdater.java +++ b/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdater.java @@ -17,6 +17,7 @@ import com.intellij.util.concurrency.EdtExecutorService; import com.intellij.util.concurrency.Invoker; import com.intellij.util.containers.SmartHashSet; import com.intellij.util.messages.MessageBusConnection; +import kotlinx.coroutines.CoroutineScope; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,6 +39,10 @@ public abstract class ProjectFileNodeUpdater { this(project, new ProjectFileNodeUpdaterLegacyInvoker(invoker)); } + public ProjectFileNodeUpdater(@NotNull Project project, @NotNull CoroutineScope coroutineScope) { + this(project, new ProjectFileNodeUpdaterCoroutineInvoker(coroutineScope)); + } + private ProjectFileNodeUpdater(@NotNull Project project, @NotNull ProjectFileNodeUpdaterInvoker invoker) { this.invoker = invoker; MessageBusConnection connection = project.getMessageBus().connect(invoker); diff --git a/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdaterCoroutineInvoker.kt b/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdaterCoroutineInvoker.kt new file mode 100644 index 000000000000..c63a28e67d6f --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdaterCoroutineInvoker.kt @@ -0,0 +1,41 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ui.tree.project + +import com.intellij.openapi.util.Disposer +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.asPromise + +internal class ProjectFileNodeUpdaterCoroutineInvoker(private val coroutineScope: CoroutineScope) : ProjectFileNodeUpdaterInvoker { + private val semaphore = Semaphore(1) + + init { + coroutineScope.coroutineContext.job.invokeOnCompletion { + Disposer.dispose(this) + } + } + + override fun invoke(runnable: Runnable): Promise<*>? { + val job = coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + semaphore.withPermit { + yield() + runnable.run() + } + } + return job.asPromise() + } + + override fun invokeLater(runnable: Runnable, delay: Int) { + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + delay(delay.toLong()) + semaphore.withPermit { + yield() + runnable.run() + } + } + } + + override fun dispose() { } +} diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index c8e42f02c40e..35ea7ea890f8 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -1038,6 +1038,8 @@ +