From 8c43d7e67e0d211524b18b2f68bbdc3e0f726b32 Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Fri, 13 Sep 2024 16:43:07 +0300 Subject: [PATCH] IJPL-158493 Add new tree model support to Project View Implement ProjectFileNodeUpdaterCoroutineInvoker and CoroutineProjectViewSupport to bridge the existing implementation of the Project View and the new tree model implementation. Add the ide.project.view.coroutines registry key to be able to enable the new implementation, but keep it disabled for now, as the new implementation likely needs a lot of fixes and improvements. GitOrigin-RevId: 532f1c18a0a48f34d4d07c474e9345f9ba4023ca --- .../impl/AbstractProjectViewPane.java | 1 - ...stractProjectViewPaneWithAsyncSupport.java | 4 +- .../impl/CoroutineProjectViewSupport.kt | 201 ++++++++++++++++++ .../impl/ProjectViewPaneSupportService.kt | 33 +++ .../platform-impl/api-dump-unreviewed.txt | 1 + .../tree/project/ProjectFileNodeUpdater.java | 5 + .../ProjectFileNodeUpdaterCoroutineInvoker.kt | 41 ++++ .../src/META-INF/LangExtensions.xml | 2 + 8 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/ide/projectView/impl/CoroutineProjectViewSupport.kt create mode 100644 platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewPaneSupportService.kt create mode 100644 platform/platform-impl/src/com/intellij/ui/tree/project/ProjectFileNodeUpdaterCoroutineInvoker.kt 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 @@ +