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
This commit is contained in:
Sergei Tachenov
2024-09-13 16:43:07 +03:00
committed by intellij-monorepo-bot
parent be921b067e
commit 8c43d7e67e
8 changed files with 286 additions and 2 deletions

View File

@@ -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;

View File

@@ -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<NodeDescriptor<?>> comparator) {
// TODO: how do we apply the new implementation to CWM?
return new AsyncProjectViewSupport(parent, myProject, createStructure(), comparator);
}
}

View File

@@ -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<in NodeDescriptor<*>>?) {
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<TreePath?>?, structures: List<TreePath?>?) {
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<TreePath>()
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<VirtualFile>) {
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<SelectInProjectViewImpl>()

View File

@@ -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<NodeDescriptor<*>>,
): 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)
}
}

View File

@@ -25660,6 +25660,7 @@ com.intellij.ui.tree.project.ProjectFileNode
- a:getVirtualFile():com.intellij.openapi.vfs.VirtualFile
a:com.intellij.ui.tree.project.ProjectFileNodeUpdater
- <init>(com.intellij.openapi.project.Project,com.intellij.util.concurrency.Invoker):V
- <init>(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

View File

@@ -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);

View File

@@ -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() { }
}

View File

@@ -1038,6 +1038,8 @@
<applicationService serviceImplementation="com.intellij.ide.projectView.impl.ProjectViewSharedSettings"/>
<projectService serviceInterface="com.intellij.ide.projectView.ProjectView"
serviceImplementation="com.intellij.ide.projectView.impl.ProjectViewImpl"/>
<registryKey defaultValue="false" key="ide.project.view.coroutines" restartRequired="true"
description="Use the new experimental coroutine-based Project View implementation"/>
<pathMacroFilter implementation="com.intellij.ide.projectView.impl.ProjectViewPathMacroFilter"/>
<projectService serviceImplementation="com.intellij.ide.projectView.impl.ProjectViewState"/>
<projectService serviceInterface="com.intellij.execution.ui.RunnerLayoutUi$Factory"