mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
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:
committed by
intellij-monorepo-bot
parent
be921b067e
commit
8c43d7e67e
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user