mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
IJPL-158493 New tree model implementation
The main new interfaces are: - TreeDomainModel - background model interface; - TreeViewModel - flow-based view model; - TreeSwingModel - Swing compatibility layer. Auxiliary classes: - SuspendingTreeVisitor - suspending TreeVisitor equivalent; - LoadingNode - converted to Kotlin and retrofitted to implement TreeViewModel. New presentation rendering is implemented in NodeRenderer. TreeUtil.getUserObject() adapted to unwrap TreeNodeViewModel instances, as at this point they don't extend DefaultMutableTreeNode. It just doesn't seem necessary, as Swing models technically don't require it. In a similar fashion, CachedTreePresentation.getCachedNode adapted to unwrap nodes as necessary, as the new implementation forces nodes to be TreeNodeViewModel instances. AbstractTreeNodeVisitor modified to call TreeUtil.getLastUserObject instead of trying to unwrap the node itself. This lets us reuse the new getLastUserObject implementation without duplicating logic. The new implementation isn't used anywhere yet, so this commit alone shouldn't affect anything. GitOrigin-RevId: 8427b6642ff1902b3b71104c3611388e1270a5e4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1f3bbf9580
commit
be921b067e
@@ -2974,6 +2974,8 @@ e:com.intellij.ui.tree.LeafState
|
||||
- s:values():com.intellij.ui.tree.LeafState[]
|
||||
com.intellij.ui.tree.LeafState$Supplier
|
||||
- a:getLeafState():com.intellij.ui.tree.LeafState
|
||||
*:com.intellij.ui.tree.SuspendingTreeVisitor
|
||||
- a:visit(javax.swing.tree.TreePath,kotlin.coroutines.Continuation):java.lang.Object
|
||||
f:com.intellij.ui.tree.TreeCollector
|
||||
- add(java.lang.Object):Z
|
||||
- get():java.util.List
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface SuspendingTreeVisitor {
|
||||
suspend fun visit(path: TreePath): TreeVisitor.Action
|
||||
}
|
||||
@@ -7040,9 +7040,17 @@ a:com.intellij.ui.ListUtil$Updatable
|
||||
- pa:update():V
|
||||
c:com.intellij.ui.LoadingNode
|
||||
- javax.swing.tree.DefaultMutableTreeNode
|
||||
- com.intellij.ui.treeStructure.TreeNodeViewModel
|
||||
- sf:Companion:com.intellij.ui.LoadingNode$Companion
|
||||
- <init>():V
|
||||
- <init>(java.lang.String):V
|
||||
- s:getText():java.lang.String
|
||||
- b:<init>(java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V
|
||||
- getChildren():kotlinx.coroutines.flow.Flow
|
||||
- getPresentation():kotlinx.coroutines.flow.Flow
|
||||
- sf:getText():java.lang.String
|
||||
- presentationSnapshot():com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
f:com.intellij.ui.LoadingNode$Companion
|
||||
- f:getText():java.lang.String
|
||||
a:com.intellij.ui.MouseDragHelper
|
||||
- java.awt.event.MouseAdapter
|
||||
- com.intellij.openapi.util.Weighted
|
||||
@@ -11105,6 +11113,56 @@ com.intellij.ui.treeStructure.TreeBulkExpansionListener
|
||||
- treeBulkCollapseStarted(com.intellij.ui.treeStructure.TreeBulkExpansionEvent):V
|
||||
- treeBulkExpansionEnded(com.intellij.ui.treeStructure.TreeBulkExpansionEvent):V
|
||||
- treeBulkExpansionStarted(com.intellij.ui.treeStructure.TreeBulkExpansionEvent):V
|
||||
*:com.intellij.ui.treeStructure.TreeDomainModel
|
||||
- a:accessData(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:computeRoot(kotlin.coroutines.Continuation):java.lang.Object
|
||||
f:com.intellij.ui.treeStructure.TreeDomainModelKt
|
||||
- *sf:TreeDomainModel(com.intellij.ide.util.treeView.AbstractTreeStructure,Z,I):com.intellij.ui.treeStructure.TreeDomainModel
|
||||
*:com.intellij.ui.treeStructure.TreeNodeDomainModel
|
||||
- a:computeChildren(kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:computeLeafState(kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:computePresentation(com.intellij.ui.treeStructure.TreeNodePresentationBuilder,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:getUserObject():java.lang.Object
|
||||
*:com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
- a:getFullText():java.util.List
|
||||
- a:getIcon():javax.swing.Icon
|
||||
- a:getMainText():java.lang.String
|
||||
- a:getToolTip():java.lang.String
|
||||
- a:isLeaf():Z
|
||||
*:com.intellij.ui.treeStructure.TreeNodePresentationBuilder
|
||||
- a:appendTextFragment(java.lang.String,com.intellij.ui.SimpleTextAttributes):V
|
||||
- a:build():com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
- a:setIcon(javax.swing.Icon):V
|
||||
- a:setMainText(java.lang.String):V
|
||||
- a:setToolTipText(java.lang.String):V
|
||||
*:com.intellij.ui.treeStructure.TreeNodeTextFragment
|
||||
- a:getAttributes():com.intellij.ui.SimpleTextAttributes
|
||||
- a:getText():java.lang.String
|
||||
*:com.intellij.ui.treeStructure.TreeNodeViewModel
|
||||
- a:getChildren():kotlinx.coroutines.flow.Flow
|
||||
- a:getPresentation():kotlinx.coroutines.flow.Flow
|
||||
- a:getUserObject():java.lang.Object
|
||||
- a:presentationSnapshot():com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
*:com.intellij.ui.treeStructure.TreeSwingModel
|
||||
- com.intellij.ui.tree.TreeVisitor$LoadingAwareAcceptor
|
||||
- com.intellij.ui.treeStructure.BgtAwareTreeModel
|
||||
- javax.swing.tree.TreeModel
|
||||
- a:getChild(java.lang.Object,I):com.intellij.ui.treeStructure.TreeNodeViewModel
|
||||
- a:getRoot():com.intellij.ui.treeStructure.TreeNodeViewModel
|
||||
- a:getShowLoadingNode():Z
|
||||
- a:setShowLoadingNode(Z):V
|
||||
f:com.intellij.ui.treeStructure.TreeSwingModelKt
|
||||
- *sf:TreeSwingModel(kotlinx.coroutines.CoroutineScope,com.intellij.ui.treeStructure.TreeViewModel):com.intellij.ui.treeStructure.TreeSwingModel
|
||||
*:com.intellij.ui.treeStructure.TreeViewModel
|
||||
- a:accept(com.intellij.ui.tree.SuspendingTreeVisitor,Z,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:accept(com.intellij.ui.tree.TreeVisitor,Z,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:getComparator():java.util.Comparator
|
||||
- a:getDomainModel():com.intellij.ui.treeStructure.TreeDomainModel
|
||||
- a:getRoot():kotlinx.coroutines.flow.Flow
|
||||
- a:invalidate(javax.swing.tree.TreePath,Z,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:setComparator(java.util.Comparator):V
|
||||
f:com.intellij.ui.treeStructure.TreeViewModelKt
|
||||
- *sf:TreeViewModel(kotlinx.coroutines.CoroutineScope,com.intellij.ui.treeStructure.TreeDomainModel):com.intellij.ui.treeStructure.TreeViewModel
|
||||
c:com.intellij.ui.treeStructure.WeightBasedComparator
|
||||
- java.util.Comparator
|
||||
- sf:FULL_INSTANCE:com.intellij.ui.treeStructure.WeightBasedComparator
|
||||
|
||||
@@ -173,7 +173,10 @@ class CachedTreePresentation(rootPresentation: CachedTreePresentationData) {
|
||||
}
|
||||
|
||||
private fun getCachedNode(node: Any): CachedTreePresentationNode? {
|
||||
return if (node is CachedTreePresentationNode) return node else cachedNodeByRealNode[node]
|
||||
if (node is CachedTreePresentationNode) return node
|
||||
val userObject = TreeUtil.getUserObject(node)
|
||||
if (userObject is CachedTreePresentationNode) return userObject
|
||||
return cachedNodeByRealNode[node]
|
||||
}
|
||||
|
||||
fun setExpanded(path: TreePath, isExpanded: Boolean) {
|
||||
|
||||
@@ -16,6 +16,9 @@ import com.intellij.openapi.util.registry.Registry;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.ui.ColoredTreeCellRenderer;
|
||||
import com.intellij.ui.SimpleTextAttributes;
|
||||
import com.intellij.ui.treeStructure.TreeNodePresentation;
|
||||
import com.intellij.ui.treeStructure.TreeNodeTextFragment;
|
||||
import com.intellij.ui.treeStructure.TreeNodeViewModel;
|
||||
import com.intellij.util.ui.StartupUiUtil;
|
||||
import com.intellij.util.ui.tree.TreeUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -35,6 +38,26 @@ public class NodeRenderer extends ColoredTreeCellRenderer {
|
||||
|
||||
@Override
|
||||
public void customizeCellRenderer(@NotNull JTree tree, @NlsSafe Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
|
||||
if (value instanceof TreeNodeViewModel vm) {
|
||||
customizeViewModelRenderer(vm.presentationSnapshot(), selected, hasFocus);
|
||||
}
|
||||
else {
|
||||
customizeLegacyRenderer(tree, value, selected, expanded, leaf, row, hasFocus);
|
||||
}
|
||||
}
|
||||
|
||||
private void customizeViewModelRenderer(@NotNull TreeNodePresentation presentation, boolean selected, boolean hasFocus) {
|
||||
setIcon(fixIconIfNeeded(presentation.getIcon(), selected, hasFocus));
|
||||
boolean isMain = true;
|
||||
for (@NotNull TreeNodeTextFragment fragment : presentation.getFullText()) {
|
||||
var simpleTextAttributes = fragment.getAttributes();
|
||||
isMain = isMain && !Comparing.equal(simpleTextAttributes.getFgColor(), SimpleTextAttributes.GRAYED_ATTRIBUTES.getFgColor());
|
||||
append(fragment.getText(), simpleTextAttributes, isMain);
|
||||
}
|
||||
setToolTipText(presentation.getToolTip());
|
||||
}
|
||||
|
||||
private void customizeLegacyRenderer(@NotNull JTree tree, @NlsSafe Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
|
||||
@NlsSafe Object node = TreeUtil.getUserObject(value);
|
||||
|
||||
if (node instanceof NodeDescriptor<?> descriptor) {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ui;
|
||||
|
||||
import com.intellij.ide.IdeBundle;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
public class LoadingNode extends DefaultMutableTreeNode {
|
||||
public LoadingNode() {
|
||||
this(getText());
|
||||
}
|
||||
|
||||
public static @NotNull @Nls String getText() {
|
||||
return IdeBundle.message("treenode.loading");
|
||||
}
|
||||
|
||||
public LoadingNode(@Nls @NotNull String text) {
|
||||
super(text);
|
||||
}
|
||||
}
|
||||
33
platform/platform-api/src/com/intellij/ui/LoadingNode.kt
Normal file
33
platform/platform-api/src/com/intellij/ui/LoadingNode.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ui
|
||||
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
import com.intellij.ui.treeStructure.TreeNodePresentationImpl
|
||||
import com.intellij.ui.treeStructure.TreeNodeTextFragmentImpl
|
||||
import com.intellij.ui.treeStructure.TreeNodeViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
|
||||
open class LoadingNode @JvmOverloads constructor(text: @Nls String = getText()) : DefaultMutableTreeNode(text), TreeNodeViewModel {
|
||||
companion object {
|
||||
@JvmStatic fun getText(): @Nls String = IdeBundle.message("treenode.loading")
|
||||
}
|
||||
|
||||
private val presentationValue = TreeNodePresentationImpl(
|
||||
isLeaf = true,
|
||||
icon = null,
|
||||
mainText = userObject as String,
|
||||
fullText = listOf(TreeNodeTextFragmentImpl(userObject as String, SimpleTextAttributes.GRAY_ATTRIBUTES)),
|
||||
toolTip = null,
|
||||
)
|
||||
|
||||
override val presentation: Flow<TreeNodePresentation> = flowOf(presentationValue)
|
||||
|
||||
override val children: Flow<List<TreeNodeViewModel>>
|
||||
get() = flowOf(emptyList())
|
||||
|
||||
override fun presentationSnapshot(): TreeNodePresentation = presentationValue
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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.treeStructure
|
||||
|
||||
import com.intellij.ide.util.treeView.AbstractTreeStructure
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.tree.LeafState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import javax.swing.Icon
|
||||
|
||||
@ApiStatus.Experimental
|
||||
fun TreeDomainModel(structure: AbstractTreeStructure, useReadAction: Boolean, concurrency: Int): TreeDomainModel =
|
||||
service<TreeDomainModelFactory>().createTreeDomainModel(structure, useReadAction, concurrency)
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeDomainModel {
|
||||
suspend fun <T> accessData(accessor: () -> T): T
|
||||
suspend fun computeRoot(): TreeNodeDomainModel?
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeNodeDomainModel {
|
||||
val userObject: Any
|
||||
suspend fun computeLeafState(): LeafState
|
||||
suspend fun computePresentation(builder: TreeNodePresentationBuilder): Flow<TreeNodePresentation>
|
||||
suspend fun computeChildren(): List<TreeNodeDomainModel>
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeNodePresentationBuilder {
|
||||
fun setIcon(icon: Icon?)
|
||||
fun setMainText(text: String)
|
||||
fun appendTextFragment(text: String, attributes: SimpleTextAttributes)
|
||||
fun setToolTipText(toolTip: String?)
|
||||
fun build(): TreeNodePresentation
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
sealed interface TreeNodePresentation {
|
||||
val isLeaf: Boolean
|
||||
val icon: Icon?
|
||||
@get:NlsSafe val mainText: String
|
||||
val fullText: List<TreeNodeTextFragment>
|
||||
@get:NlsContexts.Tooltip val toolTip: String?
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
sealed interface TreeNodeTextFragment {
|
||||
@get:NlsSafe val text: String
|
||||
val attributes: SimpleTextAttributes
|
||||
}
|
||||
|
||||
@Internal
|
||||
interface TreeDomainModelFactory {
|
||||
fun createTreeDomainModel(structure: AbstractTreeStructure, useReadAction: Boolean, concurrency: Int): TreeDomainModel
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.treeStructure
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.ui.tree.TreeVisitor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.tree.TreeModel
|
||||
|
||||
@ApiStatus.Experimental
|
||||
fun TreeSwingModel(coroutineScope: CoroutineScope, viewModel: TreeViewModel): TreeSwingModel =
|
||||
(service<TreeSwingModelFactory>()).createTreeSwingModel(coroutineScope, viewModel)
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeSwingModel : TreeModel, TreeVisitor.LoadingAwareAcceptor, BgtAwareTreeModel {
|
||||
var showLoadingNode: Boolean
|
||||
override fun getRoot(): TreeNodeViewModel?
|
||||
override fun getChild(parent: Any?, index: Int): TreeNodeViewModel?
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface TreeSwingModelFactory {
|
||||
fun createTreeSwingModel(coroutineScope: CoroutineScope, viewModel: TreeViewModel): TreeSwingModel
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.treeStructure
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.tree.SuspendingTreeVisitor
|
||||
import com.intellij.ui.tree.TreeVisitor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
@ApiStatus.Experimental
|
||||
fun TreeViewModel(coroutineScope: CoroutineScope, domainModel: TreeDomainModel): TreeViewModel =
|
||||
(service<TreeViewModelFactory>()).createTreeViewModel(coroutineScope, domainModel)
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeViewModel {
|
||||
val domainModel: TreeDomainModel
|
||||
val root: Flow<TreeNodeViewModel?>
|
||||
suspend fun invalidate(path: TreePath?, recursive: Boolean)
|
||||
var comparator: Comparator<in TreeNodeViewModel>?
|
||||
suspend fun accept(visitor: TreeVisitor, allowLoading: Boolean): TreePath?
|
||||
suspend fun accept(visitor: SuspendingTreeVisitor, allowLoading: Boolean): TreePath?
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface TreeNodeViewModel {
|
||||
fun getUserObject(): Any
|
||||
val presentation: Flow<TreeNodePresentation>
|
||||
val children: Flow<List<TreeNodeViewModel>>
|
||||
fun presentationSnapshot(): TreeNodePresentation
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface TreeViewModelFactory {
|
||||
fun createTreeViewModel(coroutineScope: CoroutineScope, domainModel: TreeDomainModel): TreeViewModel
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
data class TreeNodePresentationImpl(
|
||||
override val isLeaf: Boolean,
|
||||
override val icon: Icon?,
|
||||
override val mainText: String,
|
||||
override val fullText: List<TreeNodeTextFragment>,
|
||||
override val toolTip: String?,
|
||||
) : TreeNodePresentation
|
||||
|
||||
@ApiStatus.Internal
|
||||
data class TreeNodeTextFragmentImpl(
|
||||
override val text: String,
|
||||
override val attributes: SimpleTextAttributes,
|
||||
) : TreeNodeTextFragment
|
||||
@@ -23,6 +23,7 @@ import com.intellij.ui.tree.DelegatingEdtBgtTreeVisitor;
|
||||
import com.intellij.ui.tree.TreeVisitor;
|
||||
import com.intellij.ui.treeStructure.CachingTreePath;
|
||||
import com.intellij.ui.treeStructure.Tree;
|
||||
import com.intellij.ui.treeStructure.TreeNodeViewModel;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.Range;
|
||||
import com.intellij.util.concurrency.EdtScheduler;
|
||||
@@ -1354,7 +1355,9 @@ public final class TreeUtil {
|
||||
}
|
||||
|
||||
public static @Nullable Object getUserObject(@Nullable Object node) {
|
||||
return node instanceof DefaultMutableTreeNode ? ((DefaultMutableTreeNode)node).getUserObject() : node;
|
||||
if (node instanceof DefaultMutableTreeNode treeNode) return treeNode.getUserObject();
|
||||
if (node instanceof TreeNodeViewModel nodeModel) return nodeModel.getUserObject();
|
||||
return node;
|
||||
}
|
||||
|
||||
public static @Nullable <T> T getUserObject(@NotNull Class<T> type, @Nullable Object node) {
|
||||
|
||||
@@ -5,10 +5,10 @@ import com.intellij.ide.util.treeView.AbstractTreeNode;
|
||||
import com.intellij.ide.util.treeView.CachedTreePresentationNode;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
|
||||
import com.intellij.util.ui.tree.TreeUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -48,31 +48,19 @@ public abstract class AbstractTreeNodeVisitor<T> implements TreeVisitor {
|
||||
if (LOG.isTraceEnabled()) LOG.debug("process ", path);
|
||||
T element = getElement();
|
||||
if (element == null) return Action.SKIP_SIBLINGS;
|
||||
Object component = path.getLastPathComponent();
|
||||
if (component instanceof AbstractTreeNode) {
|
||||
return visit(path, (AbstractTreeNode)component, element);
|
||||
Object object = TreeUtil.getLastUserObject(path);
|
||||
if (object instanceof AbstractTreeNode) {
|
||||
return visit(path, (AbstractTreeNode)object, element);
|
||||
}
|
||||
if (component instanceof DefaultMutableTreeNode node) {
|
||||
Object object = node.getUserObject();
|
||||
if (object instanceof AbstractTreeNode) {
|
||||
return visit(path, (AbstractTreeNode)object, element);
|
||||
}
|
||||
else if (object instanceof String) {
|
||||
LOG.debug("ignore children: ", object);
|
||||
}
|
||||
else {
|
||||
LOG.warn(object == null ? "no object" : "unexpected object " + object.getClass());
|
||||
}
|
||||
else if (object instanceof String) {
|
||||
LOG.debug("ignore children: ", object);
|
||||
}
|
||||
else if (component instanceof CachedTreePresentationNode) {
|
||||
else if (object instanceof CachedTreePresentationNode) {
|
||||
// Cached presentation nodes don't contain the actual object because it's not loaded yet.
|
||||
return Action.SKIP_CHILDREN;
|
||||
}
|
||||
else if (component instanceof String) {
|
||||
LOG.debug("ignore children: ", component);
|
||||
}
|
||||
else {
|
||||
LOG.warn(component == null ? "no component" : "unexpected component " + component.getClass());
|
||||
LOG.warn(object == null ? "no object" : "unexpected object " + object.getClass());
|
||||
}
|
||||
return Action.SKIP_CHILDREN;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.ui.treeStructure.TreeDomainModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
internal class TreeDomainModelDelegatingVisitor(
|
||||
private val model: TreeDomainModel,
|
||||
private val delegate: TreeVisitor,
|
||||
) : SuspendingTreeVisitor {
|
||||
|
||||
override suspend fun visit(path: TreePath): TreeVisitor.Action {
|
||||
if (delegate.visitThread() == TreeVisitor.VisitThread.EDT) {
|
||||
return actualVisitEdt(delegate, path) // For EDT visiting, the visitor does all three steps in visit().
|
||||
}
|
||||
else {
|
||||
val preVisitResult = preVisit(path, delegate)
|
||||
if (preVisitResult != null) return preVisitResult
|
||||
val visitResult = actualVisitBgt(delegate, path)
|
||||
val postVisitResult = postVisit(visitResult, path, delegate)
|
||||
return postVisitResult
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun preVisit(path: TreePath, visitor: TreeVisitor): TreeVisitor.Action? =
|
||||
(visitor as? EdtBgtTreeVisitor)?.let {
|
||||
withContext(Dispatchers.EDT) {
|
||||
visitor.preVisitEDT(path)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun actualVisitEdt(
|
||||
visitor: TreeVisitor,
|
||||
path: TreePath,
|
||||
): TreeVisitor.Action =
|
||||
withContext(Dispatchers.EDT) {
|
||||
visitor.visit(path)
|
||||
}
|
||||
|
||||
private suspend fun actualVisitBgt(
|
||||
visitor: TreeVisitor,
|
||||
path: TreePath,
|
||||
): TreeVisitor.Action =
|
||||
withContext(Dispatchers.Default) {
|
||||
model.accessData {
|
||||
visitor.visit(path)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun postVisit(action: TreeVisitor.Action, path: TreePath, visitor: TreeVisitor): TreeVisitor.Action =
|
||||
(visitor as? EdtBgtTreeVisitor)?.let {
|
||||
withContext(Dispatchers.EDT) {
|
||||
visitor.postVisitEDT(path, action)
|
||||
}
|
||||
} ?: action
|
||||
|
||||
override fun toString(): String =
|
||||
"TreeDomainModelDelegatingVisitor(model=$model, delegate=$delegate)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.ide.util.treeView.AbstractTreeStructure
|
||||
import com.intellij.ui.treeStructure.TreeDomainModel
|
||||
import com.intellij.ui.treeStructure.TreeDomainModelFactory
|
||||
|
||||
internal class TreeDomainModelFactoryImpl : TreeDomainModelFactory {
|
||||
override fun createTreeDomainModel(structure: AbstractTreeStructure, useReadAction: Boolean, concurrency: Int): TreeDomainModel =
|
||||
TreeStructureDomainModelAdapter(structure, useReadAction, concurrency)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.ide.util.treeView.*
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.progress.blockingContext
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.treeStructure.TreeDomainModel
|
||||
import com.intellij.ui.treeStructure.TreeNodeDomainModel
|
||||
import com.intellij.ui.treeStructure.TreeNodePresentation
|
||||
import com.intellij.ui.treeStructure.TreeNodePresentationBuilder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
|
||||
internal class TreeStructureDomainModelAdapter(
|
||||
private val structure: AbstractTreeStructure,
|
||||
private val useReadAction: Boolean,
|
||||
concurrency: Int,
|
||||
) : TreeDomainModel {
|
||||
private val semaphore = Semaphore(concurrency)
|
||||
|
||||
override suspend fun computeRoot(): TreeNodeDomainModel? = accessData {
|
||||
structure.rootElement.validate()?.let { TreeStructureNodeDomainModel(structure.createDescriptor(it, null)) }
|
||||
}
|
||||
|
||||
override suspend fun <T> accessData(accessor: () -> T): T = semaphore.withPermit {
|
||||
if (useReadAction) {
|
||||
readAction {
|
||||
accessor()
|
||||
}
|
||||
}
|
||||
else {
|
||||
blockingContext {
|
||||
accessor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any.validate(): Any? {
|
||||
if (this is AbstractTreeNode<*> && value == null) return null
|
||||
if (this is ValidateableNode && !isValid) return null
|
||||
if (!structure.isValid(this)) return null
|
||||
return this
|
||||
}
|
||||
|
||||
private inner class TreeStructureNodeDomainModel(override val userObject: NodeDescriptor<*>) : TreeNodeDomainModel {
|
||||
override suspend fun computeLeafState(): LeafState = accessData {
|
||||
structure.getLeafState(userObject.element)
|
||||
}
|
||||
|
||||
override suspend fun computePresentation(builder: TreeNodePresentationBuilder): Flow<TreeNodePresentation> =
|
||||
flowOf(accessData {
|
||||
buildPresentation(builder, userObject)
|
||||
})
|
||||
|
||||
override suspend fun computeChildren(): List<TreeNodeDomainModel> = accessData {
|
||||
userObject.element.validate()?.let { parentElement ->
|
||||
structure.getChildElements(parentElement)
|
||||
.asSequence()
|
||||
.mapNotNull { it.validate() }
|
||||
.map { childElement ->
|
||||
TreeStructureNodeDomainModel(structure.createDescriptor(childElement, userObject))
|
||||
}
|
||||
.toList()
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "TreeStructureNodeDomainModel(userObject=$userObject)"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TreeStructureNodeDomainModel
|
||||
|
||||
return userObject == other.userObject
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return userObject.hashCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildPresentation(builder: TreeNodePresentationBuilder, userObject: Any): TreeNodePresentation {
|
||||
if (userObject !is PresentableNodeDescriptor<*>) {
|
||||
builder.setMainText(userObject.toString())
|
||||
return builder.build()
|
||||
}
|
||||
userObject.update()
|
||||
val presentation = userObject.presentation
|
||||
return builder.run {
|
||||
setIcon(presentation.getIcon(false))
|
||||
setMainText(presentation.presentableText ?: "")
|
||||
for (fragment in presentation.coloredText) {
|
||||
appendTextFragment(fragment.text, fragment.attributes ?: SimpleTextAttributes.REGULAR_ATTRIBUTES)
|
||||
}
|
||||
val location = presentation.locationString
|
||||
if (!location.isNullOrEmpty()) {
|
||||
val prefix = presentation.locationPrefix
|
||||
val suffix = presentation.locationSuffix
|
||||
appendTextFragment(prefix + location + suffix, SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||
}
|
||||
setToolTipText(presentation.tooltip)
|
||||
build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.ide.util.treeView.CachedTreePresentation
|
||||
import com.intellij.ide.util.treeView.CachedTreePresentationSupport
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.ui.LoadingNode
|
||||
import com.intellij.ui.treeStructure.*
|
||||
import com.intellij.util.ui.tree.TreeModelListenerList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.concurrency.AsyncPromise
|
||||
import org.jetbrains.concurrency.Promise
|
||||
import org.jetbrains.concurrency.isPending
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
internal class TreeSwingModelFactoryImpl : TreeSwingModelFactory {
|
||||
override fun createTreeSwingModel(coroutineScope: CoroutineScope, viewModel: TreeViewModel): TreeSwingModel =
|
||||
TreeSwingModelImpl(coroutineScope, viewModel)
|
||||
}
|
||||
|
||||
private class TreeSwingModelImpl(
|
||||
parentScope: CoroutineScope,
|
||||
private val viewModel: TreeViewModel,
|
||||
) : TreeSwingModel, CachedTreePresentationSupport {
|
||||
private val treeScope = parentScope.childScope("Root of $this", Dispatchers.EDT)
|
||||
private val listeners = TreeModelListenerList()
|
||||
private var root: Node? = null
|
||||
// These things must be thread-safe because of the "cancellation can happen anywhere, anytime" thing.
|
||||
private val nodes = ConcurrentHashMap<TreeNodeViewModel, Node>()
|
||||
private val nodeLoadedListeners = CopyOnWriteArrayList<NodeLoadedListener>()
|
||||
private var cachedPresentation: CachedTreePresentation? = null
|
||||
|
||||
override var showLoadingNode: Boolean = true
|
||||
|
||||
private interface NodeLoadedListener {
|
||||
fun nodePublished(node: Node)
|
||||
}
|
||||
|
||||
init {
|
||||
treeScope.launch(CoroutineName("Root updates of $this")) {
|
||||
viewModel.root.collect { rootViewModel ->
|
||||
val root = rootViewModel?.let {
|
||||
findOrLoadNode(null, rootViewModel)
|
||||
}
|
||||
if (this@TreeSwingModelImpl.root != root) {
|
||||
this@TreeSwingModelImpl.root?.dispose()
|
||||
this@TreeSwingModelImpl.root = root
|
||||
if (root != null) {
|
||||
cachedPresentation?.rootLoaded(root.viewModel)
|
||||
}
|
||||
treeStructureChanged(root)
|
||||
if (root != null) {
|
||||
markPublished(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoot(): TreeNodeViewModel? = root?.viewModel
|
||||
|
||||
override fun getChild(parent: Any?, index: Int): TreeNodeViewModel? = getChildren(parent).getOrNull(index)?.viewModel
|
||||
|
||||
override fun getChildCount(parent: Any?): Int = getChildren(parent).size
|
||||
|
||||
override fun getIndexOfChild(parent: Any?, child: Any): Int = getChildren(parent).indexOfFirst { it == child }
|
||||
|
||||
override fun isLeaf(nodeViewModel: Any?): Boolean {
|
||||
if (nodeViewModel == null) return true
|
||||
nodeViewModel as TreeNodeViewModel
|
||||
return nodes[nodeViewModel]?.isLeaf ?: return true
|
||||
}
|
||||
|
||||
private fun getChildren(nodeViewModel: Any?): List<Node> {
|
||||
if (nodeViewModel == null) return emptyList()
|
||||
nodeViewModel as TreeNodeViewModel
|
||||
val node = nodes[nodeViewModel] ?: return emptyList()
|
||||
node.ensureChildrenAreLoading()
|
||||
return node.children ?: emptyList()
|
||||
}
|
||||
|
||||
override fun valueForPathChanged(path: TreePath, newValue: Any) {
|
||||
throw UnsupportedOperationException("Not an editable model")
|
||||
}
|
||||
|
||||
override fun setCachedPresentation(presentation: CachedTreePresentation?) {
|
||||
this.cachedPresentation = presentation
|
||||
if (root == null && presentation != null) {
|
||||
root = createCachedNode(presentation, null, presentation.getRoot())
|
||||
treeStructureChanged(root)
|
||||
}
|
||||
}
|
||||
|
||||
private fun treeStructureChanged(node: Node?) {
|
||||
listeners.treeStructureChanged(TreeModelEvent(this, node?.path, null, null))
|
||||
}
|
||||
|
||||
private fun treeNodesRemoved(parent: Node, nodes: Map<Node, Int>) {
|
||||
fireEvent(parent, nodes) {
|
||||
treeNodesRemoved(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun treeNodesInserted(parent: Node, nodes: Map<Node, Int>) {
|
||||
fireEvent(parent, nodes) {
|
||||
treeNodesInserted(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun treeNodesChanged(parent: Node, nodes: Map<Node, Int>?) {
|
||||
fireEvent(parent, nodes) {
|
||||
treeNodesChanged(it)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun fireEvent(parent: Node, nodes: Map<Node, Int>?, event: TreeModelListenerList.(TreeModelEvent) -> Unit) {
|
||||
// A special case of nodes == null means that the parent itself has changed.
|
||||
if (nodes?.isEmpty() == true || listeners.isEmpty) return
|
||||
val indices = nodes?.values?.toIntArray()
|
||||
val values = nodes?.keys?.map { it.viewModel }?.toTypedArray()
|
||||
listeners.event(TreeModelEvent(this, parent.path, indices, values))
|
||||
}
|
||||
|
||||
private fun addNodeLoadedListener(listener: NodeLoadedListener) {
|
||||
nodeLoadedListeners += listener
|
||||
}
|
||||
|
||||
private fun removeNodeLoadedListener(listener: NodeLoadedListener) {
|
||||
nodeLoadedListeners -= listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the node as published and notifies the waiting visitors.
|
||||
*
|
||||
* A node is first created, then its presentation and isLeaf properties are loaded,
|
||||
* then the usual tree listeners are notified, and only then the node can be visited.
|
||||
* This is because some visitors interact with the tree and its UI,
|
||||
* for example, to expand the node being visited,
|
||||
* and this is only possible once the UI (which is a regular listener) is notified.
|
||||
*/
|
||||
private fun markPublished(node: Node) {
|
||||
if (node.state == NodeState.PUBLISHED) return
|
||||
if (node.state != NodeState.LOADED) {
|
||||
LOG.warn(Throwable("Marking the node as published, but it's not loaded: $node"))
|
||||
// proceed anyway, as an inconsistent state is still better than a visitor waiting forever
|
||||
}
|
||||
node.state = NodeState.PUBLISHED
|
||||
nodeLoadedListeners.forEach { it.nodePublished(node) }
|
||||
}
|
||||
|
||||
override fun accept(visitor: TreeVisitor, allowLoading: Boolean): Promise<TreePath?> {
|
||||
val promise = AsyncPromise<TreePath?>()
|
||||
val job = treeScope.launch(CoroutineName("Accept $visitor")) {
|
||||
try {
|
||||
promise.setResult(viewModel.accept(SwingAwareVisitorDelegate(visitor, allowLoading), allowLoading))
|
||||
}
|
||||
catch (e: Exception) {
|
||||
if (e !is CancellationException) promise.setError(e)
|
||||
}
|
||||
}
|
||||
job.invokeOnCompletion {
|
||||
if (promise.isPending) {
|
||||
SwingUtilities.invokeLater { promise.cancel() }
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
private inner class SwingAwareVisitorDelegate(delegate: TreeVisitor, private val allowLoading: Boolean) : SuspendingTreeVisitor {
|
||||
private val viewModelVisitor = TreeDomainModelDelegatingVisitor(viewModel.domainModel, delegate)
|
||||
|
||||
override suspend fun visit(path: TreePath): TreeVisitor.Action {
|
||||
val nodeViewModel = path.lastPathComponent as TreeNodeViewModel
|
||||
// Before visiting, we must make sure that the node we're about to visit actually exists in this model.
|
||||
// For the root node, it means that it was already collected and fully loaded.
|
||||
// For child nodes, it means that its parent's children are loaded.
|
||||
// In both cases, it's very important that it isn't just created, but actually reported to the tree and its UI!
|
||||
// Otherwise, Swing can be in an inconsistent state: the node exists, but, for example, can't be expanded.
|
||||
// So here it's very important to wait until it's fully loaded and reported via the listeners.
|
||||
// The view model invokes this whole thing under the node view mode's scope,
|
||||
// so we can be sure we won't wait forever:
|
||||
// if the node still exists, it'll make its way here sooner or later,
|
||||
// otherwise its scope will be canceled along with our waiting.
|
||||
val node = awaitNode(nodeViewModel)
|
||||
val result = viewModelVisitor.visit(path)
|
||||
if (result == TreeVisitor.Action.CONTINUE && allowLoading) {
|
||||
// This is needed to ensure that awaitNode() calls for children won't wait forever,
|
||||
// because we don't even try to load nodes unless they're requested.
|
||||
node.ensureChildrenAreLoading()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun awaitNode(viewModel: TreeNodeViewModel): Node = suspendCancellableCoroutine { continuation ->
|
||||
// This whole thing works strictly on the EDT, so no worries about concurrency issues here.
|
||||
// If we check the node, and it's not loaded yet,
|
||||
// it's guaranteed that it won't suddenly become loaded before we register the listener.
|
||||
// The only possibly non-EDT part here is the invokeOnCancellation() call, but it's just last-chance cleanup.
|
||||
val existingNode = nodes[viewModel]
|
||||
if (existingNode?.state == NodeState.PUBLISHED) {
|
||||
continuation.resumeWith(Result.success(existingNode))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
val listener = object : NodeLoadedListener {
|
||||
override fun nodePublished(node: Node) {
|
||||
if (node.viewModel == viewModel) {
|
||||
removeNodeLoadedListener(this)
|
||||
continuation.resumeWith(Result.success(node))
|
||||
}
|
||||
}
|
||||
}
|
||||
addNodeLoadedListener(listener)
|
||||
continuation.invokeOnCancellation { removeNodeLoadedListener(listener) }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SwingAwareVisitorDelegate(allowLoading=$allowLoading, viewModelVisitor=$viewModelVisitor)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCachedNode(treePresentation: CachedTreePresentation, parent: Node?, cachedObject: Any): Node {
|
||||
val cachedViewModel = cachedObject as? CachedViewModel ?: CachedViewModel(treePresentation, cachedObject)
|
||||
val cachedNode = CachedNode(treePresentation, parent, cachedViewModel)
|
||||
nodes[cachedNode.viewModel] = cachedNode
|
||||
return cachedNode
|
||||
}
|
||||
|
||||
private suspend fun loadNode(parent: RealNode?, viewModel: TreeNodeViewModel): Node? {
|
||||
val existingNode = nodes[viewModel]
|
||||
if (existingNode != null) {
|
||||
LOG.warn(Throwable("The node $viewModel already exists elsewhere as $existingNode"))
|
||||
return null
|
||||
}
|
||||
var result = RealNode(parent, viewModel)
|
||||
nodes[viewModel] = result
|
||||
return result.awaitLoaded()
|
||||
}
|
||||
|
||||
private suspend fun findOrLoadNode(parent: RealNode?, viewModel: TreeNodeViewModel): Node? {
|
||||
val existingNode = nodes[viewModel]
|
||||
val result = when (existingNode?.state) {
|
||||
NodeState.CACHED -> {
|
||||
LOG.warn(Throwable("Attempt to load a cached $viewModel: $existingNode"))
|
||||
null
|
||||
}
|
||||
// We have to call awaitLoaded() even if it's already loaded, to ensure that the presentation is up to date.
|
||||
NodeState.CREATED, NodeState.LOADED, NodeState.PUBLISHED -> existingNode.awaitLoaded()
|
||||
NodeState.DISPOSED -> {
|
||||
LOG.warn(Throwable("Attempt to load $viewModel when its node has already been disposed: $existingNode"))
|
||||
null
|
||||
}
|
||||
null -> loadNode(parent, viewModel)
|
||||
}
|
||||
if (result != null && result.path.parentPath != parent?.path) {
|
||||
LOG.warn(Throwable("Attempt to load $viewModel when it has already been registered: $existingNode"))
|
||||
return null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun addTreeModelListener(l: TreeModelListener) {
|
||||
listeners.add(l)
|
||||
}
|
||||
|
||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
||||
listeners.remove(l)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SwingTreeViewModel@${System.identityHashCode(this)}(viewModel=$viewModel)"
|
||||
}
|
||||
|
||||
private inner class RealNode(
|
||||
parent: RealNode?,
|
||||
override val viewModel: TreeNodeViewModel,
|
||||
) : Node {
|
||||
// Must be thread-safe because set by cancellation.
|
||||
private val stateReference = AtomicReference(NodeState.CREATED)
|
||||
|
||||
override var state: NodeState
|
||||
get() = stateReference.get()
|
||||
set(value) = stateReference.set(value)
|
||||
|
||||
override val path: CachingTreePath = parent?.path?.pathByAddingChild(viewModel) as CachingTreePath? ?: CachingTreePath(viewModel)
|
||||
val nodeScope: CoroutineScope = (parent?.nodeScope ?: treeScope).childScope(path.toString())
|
||||
|
||||
private var childrenLoadingJob: Job? = null
|
||||
|
||||
override var children: List<Node>? = null
|
||||
|
||||
override var isLeaf: Boolean = true
|
||||
|
||||
init {
|
||||
nodeScope.coroutineContext.job.invokeOnCompletion {
|
||||
state = NodeState.DISPOSED
|
||||
nodes.remove(viewModel)
|
||||
}
|
||||
nodeScope.launch(CoroutineName("Presentation updates of $this")) {
|
||||
viewModel.presentation.collectLatest { presentation ->
|
||||
// Only fire value updates after the node has been published to the UI part of the tree.
|
||||
// For two reasons: to avoid unnecessary updates (optimization) and to avoid confusing the UI state.
|
||||
if (state == NodeState.PUBLISHED) {
|
||||
isLeaf = presentation.isLeaf
|
||||
treeNodesChanged(this@RealNode, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitLoaded(): Node? {
|
||||
val job = nodeScope.launch {
|
||||
isLeaf = viewModel.presentation.first().isLeaf
|
||||
}
|
||||
job.join()
|
||||
// If the node was canceled, then either this job will be canceled or,
|
||||
// if the cancellation happened after the job has completed, the node will be disposed.
|
||||
// There's also a slight chance of it being asynchronously canceled right now,
|
||||
// but then we care little about it: some code will notice it later and get rid of it.
|
||||
// There shouldn't be much async stuff here anyway, as this thing is mostly-EDT,
|
||||
// except maybe the case of an external async cancellation.
|
||||
return if (job.isCancelled || state == NodeState.DISPOSED) {
|
||||
null
|
||||
}
|
||||
else {
|
||||
// Possible states:
|
||||
// CREATED → it's the first load attempt, mark as loaded;
|
||||
// LOADED or VISITABLE → already loaded, do nothing;
|
||||
// DISPOSED → handled above, unless it just happened, but then we don't care.
|
||||
stateReference.compareAndSet(NodeState.CREATED, NodeState.LOADED)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
override fun ensureChildrenAreLoading() {
|
||||
if (childrenLoadingJob != null) return
|
||||
val cachedChildren = getChildrenFromCachedPresentation()
|
||||
if (cachedChildren != null) {
|
||||
children = cachedChildren
|
||||
}
|
||||
else if (this@TreeSwingModelImpl.showLoadingNode) {
|
||||
// Need this for clients who expect the "loading..." node to appear immediately,
|
||||
// e.g. for com.intellij.ide.projectView.impl.ProjectViewDirectoryExpandDurationMeasurer.
|
||||
children = listOf(RealNode(this, LoadingNode()))
|
||||
}
|
||||
childrenLoadingJob = nodeScope.launch(CoroutineName("Load children of $this")) {
|
||||
viewModel.children.collect { loaded ->
|
||||
val children = loaded.map { childViewModel ->
|
||||
async(CoroutineName("Load $childViewModel")) {
|
||||
findOrLoadNode(this@RealNode, childViewModel)
|
||||
}
|
||||
}.awaitAll()
|
||||
updateChildren(children.filterNotNull().toSet().toList()) // remove duplicates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChildrenFromCachedPresentation(): List<Node>? =
|
||||
cachedPresentation?.let { cachedPresentation ->
|
||||
cachedPresentation.getChildren(viewModel)?.map { child ->
|
||||
createCachedNode(cachedPresentation, this, child)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChildren(children: List<Node>) {
|
||||
val removedChildren = this.children?.withIndex()?.associateTo(mutableMapOf()) { it.value to it.index } ?: hashMapOf()
|
||||
val insertedChildren = children.withIndex().associateTo(mutableMapOf()) { it.value to it.index }
|
||||
val changedChildren = removedChildren.keys.intersect(insertedChildren.keys)
|
||||
.associateWith { insertedChildren.getValue(it) }
|
||||
removedChildren.keys -= changedChildren.keys
|
||||
insertedChildren.keys -= changedChildren.keys
|
||||
|
||||
for (removedChild in removedChildren.keys) {
|
||||
removedChild.dispose()
|
||||
}
|
||||
|
||||
this.children = children
|
||||
|
||||
cachedPresentation?.childrenLoaded(viewModel, children.map { it.viewModel })
|
||||
|
||||
treeNodesRemoved(this, removedChildren)
|
||||
treeNodesInserted(this, insertedChildren)
|
||||
treeNodesChanged(this, changedChildren)
|
||||
|
||||
for (newChild in insertedChildren.keys) {
|
||||
markPublished(newChild)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
nodeScope.cancel()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Node(" +
|
||||
"viewModel=$viewModel, " +
|
||||
"path=$path, " +
|
||||
"isLeaf=$isLeaf, " +
|
||||
"state=$state, " +
|
||||
"${children?.size} children (${if (childrenLoadingJob == null) "not loading" else "loading"})" +
|
||||
")"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Node
|
||||
|
||||
return viewModel == other.viewModel
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return viewModel.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CachedNode(
|
||||
treePresentation: CachedTreePresentation,
|
||||
parent: Node?,
|
||||
override val viewModel: CachedViewModel
|
||||
) : Node {
|
||||
override val path: CachingTreePath = parent?.path?.pathByAddingChild(viewModel) as CachingTreePath? ?: CachingTreePath(viewModel)
|
||||
|
||||
override val isLeaf: Boolean
|
||||
get() = viewModel.cachedPresentation.isLeaf
|
||||
|
||||
override val children: List<Node>? = viewModel.cachedChildren?.map { child -> createCachedNode(treePresentation, this, child) }
|
||||
|
||||
override var state: NodeState = NodeState.CACHED
|
||||
|
||||
override fun ensureChildrenAreLoading() { }
|
||||
|
||||
override suspend fun awaitLoaded(): Node? = this
|
||||
|
||||
override fun dispose() {
|
||||
state = NodeState.DISPOSED
|
||||
nodes.remove(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class NodeState {
|
||||
CACHED,
|
||||
CREATED,
|
||||
LOADED,
|
||||
PUBLISHED,
|
||||
DISPOSED
|
||||
}
|
||||
|
||||
private sealed interface Node {
|
||||
val path: CachingTreePath
|
||||
val viewModel: TreeNodeViewModel
|
||||
val isLeaf: Boolean
|
||||
val children: List<Node>?
|
||||
var state: NodeState
|
||||
fun ensureChildrenAreLoading()
|
||||
suspend fun awaitLoaded(): Node?
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
private class CachedViewModel(
|
||||
treePresentation: CachedTreePresentation,
|
||||
private val cachedObject: Any,
|
||||
) : TreeNodeViewModel {
|
||||
val cachedPresentation: TreeNodePresentation = buildCachedPresentation(treePresentation, cachedObject)
|
||||
val cachedChildren: List<CachedViewModel>? = treePresentation.getChildren(cachedObject)?.map {
|
||||
CachedViewModel(treePresentation, it)
|
||||
}
|
||||
|
||||
override fun getUserObject(): Any = cachedObject
|
||||
|
||||
override val presentation: Flow<TreeNodePresentation>
|
||||
get() = flowOf(cachedPresentation)
|
||||
|
||||
override val children: Flow<List<TreeNodeViewModel>>
|
||||
get() = cachedChildren?.let { flowOf(it) } ?: flowOf(emptyList())
|
||||
|
||||
override fun presentationSnapshot(): TreeNodePresentation = cachedPresentation
|
||||
|
||||
override fun toString(): String {
|
||||
return "CachedViewModel(cachedObject=$cachedObject)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCachedPresentation(treePresentation: CachedTreePresentation, cachedObject: Any): TreeNodePresentation {
|
||||
val builder = TreeNodePresentationBuilderImpl(treePresentation.isLeaf(cachedObject))
|
||||
buildPresentation(builder, cachedObject)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private val LOG = logger<TreeSwingModelImpl>()
|
||||
@@ -0,0 +1,452 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package com.intellij.ui.tree
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.treeStructure.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.Icon
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
internal class TreeViewModelFactoryImpl : TreeViewModelFactory {
|
||||
override fun createTreeViewModel(coroutineScope: CoroutineScope, domainModel: TreeDomainModel): TreeViewModel =
|
||||
TreeViewModelImpl(coroutineScope, domainModel)
|
||||
}
|
||||
|
||||
private class TreeViewModelImpl(private val treeScope: CoroutineScope, override val domainModel: TreeDomainModel) : TreeViewModel {
|
||||
private val rootUpdateRequests = MutableSharedFlow<Unit>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val rootViewModelFlow = MutableSharedFlow<TreeNodeViewModelImpl?>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val lastComputedRoot = AtomicReference<TreeNodeViewModelImpl?>()
|
||||
|
||||
override val root: Flow<TreeNodeViewModel?>
|
||||
get() = rootViewModelFlow
|
||||
|
||||
private val comparatorRef = AtomicReference<Comparator<in TreeNodeViewModel>?>()
|
||||
|
||||
override var comparator: Comparator<in TreeNodeViewModel>?
|
||||
get() = comparatorRef.get()
|
||||
set(value) {
|
||||
comparatorRef.set(value)
|
||||
treeScope.launch(CoroutineName("Invalidating on comparator change for $this")) {
|
||||
invalidate(null, true)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
treeScope.launch(CoroutineName("Root updates for $this")) {
|
||||
rootUpdateRequests.collectLatest {
|
||||
val rootDomainModel = domainModel.computeRoot()
|
||||
if (rootDomainModel == null) {
|
||||
lastComputedRoot.set(null)
|
||||
rootViewModelFlow.emit(null)
|
||||
}
|
||||
else {
|
||||
val previousModel = lastComputedRoot.get()
|
||||
val newLeafState = rootDomainModel.computeLeafState()
|
||||
if (previousModel == null || previousModel.domainModel != rootDomainModel || previousModel.leafStateValue != newLeafState) {
|
||||
previousModel?.nodeScope?.cancel()
|
||||
val newRoot = rootDomainModel.toViewModel(treeScope, newLeafState, comparatorRef)
|
||||
lastComputedRoot.set(newRoot)
|
||||
rootViewModelFlow.emit(newRoot)
|
||||
}
|
||||
else {
|
||||
previousModel.updatePresentation()
|
||||
previousModel.updateChildren()
|
||||
rootViewModelFlow.emit(previousModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadRoot()
|
||||
}
|
||||
|
||||
private fun loadRoot() {
|
||||
rootViewModelFlow.resetReplayCache()
|
||||
check(rootUpdateRequests.tryEmit(Unit))
|
||||
}
|
||||
|
||||
override suspend fun invalidate(path: TreePath?, recursive: Boolean) {
|
||||
val job = treeScope.launch(CoroutineName("Invalidate $path, recursive=$recursive")) {
|
||||
val node = path?.node ?: lastComputedRoot.get()
|
||||
node?.invalidate(recursive)
|
||||
if (path == null) {
|
||||
loadRoot()
|
||||
}
|
||||
}
|
||||
job.join()
|
||||
}
|
||||
|
||||
override suspend fun accept(visitor: TreeVisitor, allowLoading: Boolean): TreePath? = acceptImpl(visitor, allowLoading)
|
||||
|
||||
override suspend fun accept(visitor: SuspendingTreeVisitor, allowLoading: Boolean): TreePath? = acceptImpl(visitor, allowLoading)
|
||||
|
||||
private suspend fun <T> acceptImpl(visitor: T, allowLoading: Boolean): TreePath? {
|
||||
val root = getFlowValue(rootViewModelFlow, allowLoading)
|
||||
if (root == null) return null
|
||||
return visit(null, listOf(root), visitor, allowLoading)
|
||||
}
|
||||
|
||||
private suspend fun <T> visit(parentPath: TreePath?, nodes: List<TreeNodeViewModelImpl>, visitor: T, allowLoading: Boolean): TreePath? {
|
||||
for (node in nodes) {
|
||||
val path = parentPath?.pathByAddingChild(node) ?: CachingTreePath(node)
|
||||
if (!node.awaitPresentation()) {
|
||||
continue // The node was canceled and disappeared.
|
||||
}
|
||||
val visit = try {
|
||||
node.nodeScope.async(CoroutineName("Visiting $node for $visitor")) {
|
||||
// Either a visitor is "smart" and knows how to handle contexts,
|
||||
if (visitor is SuspendingTreeVisitor) {
|
||||
visitor.visit(path)
|
||||
}
|
||||
// or we have to wrap it into the proper context,
|
||||
else if (visitor is TreeVisitor) {
|
||||
// which is either EDT
|
||||
if (visitor.visitThread() == TreeVisitor.VisitThread.EDT) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
visitor.visit(path)
|
||||
}
|
||||
}
|
||||
// or whatever the model considers the proper context (usually a read action).
|
||||
else {
|
||||
domainModel.accessData {
|
||||
visitor.visit(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
thisLogger<TreeViewModelImpl>().error(Throwable("Unknown visitor type: $visitor"))
|
||||
TreeVisitor.Action.SKIP_SIBLINGS
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
catch (_: CancellationException) {
|
||||
currentCoroutineContext().ensureActive() // Throw if OUR coroutine was canceled.
|
||||
TreeVisitor.Action.SKIP_CHILDREN // Skip the node with its children if ITS scope was canceled.
|
||||
}
|
||||
when (visit) {
|
||||
TreeVisitor.Action.INTERRUPT -> return path
|
||||
TreeVisitor.Action.CONTINUE -> {
|
||||
if (allowLoading) {
|
||||
node.ensureChildrenAreLoading() // Otherwise getFlowValue() may wait forever.
|
||||
}
|
||||
val children = getFlowValue(node.childrenFlow, allowLoading) ?: continue
|
||||
val result = visit(path, children, visitor, allowLoading) ?: continue
|
||||
return result
|
||||
}
|
||||
TreeVisitor.Action.SKIP_CHILDREN -> continue
|
||||
TreeVisitor.Action.SKIP_SIBLINGS -> break
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "TreeViewModelImpl@${System.identityHashCode(this)}(domainModel=$domainModel)"
|
||||
}
|
||||
}
|
||||
|
||||
internal class TreeNodePresentationBuilderImpl(val isLeaf: Boolean) : TreeNodePresentationBuilder {
|
||||
// these "Value" suffixes to avoid signature clashes with the setters
|
||||
private var iconValue: Icon? = null
|
||||
private var mainTextValue: String? = null
|
||||
private var fullTextValue: MutableList<TreeNodeTextFragment>? = null
|
||||
private var toolTipValue: String? = null
|
||||
|
||||
override fun setIcon(icon: Icon?) {
|
||||
this.iconValue = icon
|
||||
}
|
||||
|
||||
override fun setMainText(text: String) {
|
||||
this.mainTextValue = text
|
||||
}
|
||||
|
||||
override fun appendTextFragment(text: String, attributes: SimpleTextAttributes) {
|
||||
val coloredText = this.fullTextValue ?: mutableListOf()
|
||||
coloredText.add(TreeNodeTextFragmentImpl(text, attributes))
|
||||
this.fullTextValue = coloredText
|
||||
}
|
||||
|
||||
override fun setToolTipText(toolTip: String?) {
|
||||
this.toolTipValue = toolTip
|
||||
}
|
||||
|
||||
override fun build(): TreeNodePresentation {
|
||||
val specifiedMainText = this.mainTextValue
|
||||
val specifiedFullText = this.fullTextValue
|
||||
val mainText: String
|
||||
val fullText: List<TreeNodeTextFragment>
|
||||
if (specifiedMainText != null) {
|
||||
if (specifiedFullText != null) {
|
||||
mainText = specifiedMainText
|
||||
fullText = specifiedFullText
|
||||
}
|
||||
else {
|
||||
mainText = specifiedMainText
|
||||
fullText = buildColoredText(mainText)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (specifiedFullText != null) {
|
||||
mainText = buildMainText(specifiedFullText)
|
||||
fullText = specifiedFullText
|
||||
}
|
||||
else {
|
||||
throw IllegalStateException("Either the main text or the full text must be specified")
|
||||
}
|
||||
}
|
||||
return TreeNodePresentationImpl(
|
||||
isLeaf = isLeaf,
|
||||
icon = iconValue,
|
||||
mainText = mainText,
|
||||
fullText = fullText,
|
||||
toolTip = toolTipValue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildColoredText(mainText: String): List<TreeNodeTextFragmentImpl> =
|
||||
listOf(TreeNodeTextFragmentImpl(mainText, SimpleTextAttributes.REGULAR_ATTRIBUTES))
|
||||
|
||||
private fun buildMainText(fullText: List<TreeNodeTextFragment>): String {
|
||||
val builder = StringBuilder()
|
||||
for (fragment in fullText) {
|
||||
val attributes = fragment.attributes
|
||||
if (attributes.fgColor == SimpleTextAttributes.GRAYED_ATTRIBUTES.fgColor) break
|
||||
builder.append(fragment.text)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private class TreeNodeViewModelImpl(
|
||||
val nodeScope: CoroutineScope,
|
||||
val domainModel: TreeNodeDomainModel,
|
||||
val leafStateValue: LeafState,
|
||||
val comparator: AtomicReference<Comparator<in TreeNodeViewModel>?>,
|
||||
) : TreeNodeViewModel {
|
||||
private val presentationLoaded = AtomicBoolean()
|
||||
private val presentationUpdateRequests = MutableSharedFlow<Unit>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val childrenLoaded = AtomicBoolean()
|
||||
private val childrenUpdateRequests = MutableSharedFlow<Unit>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
private val presentationFlow = MutableSharedFlow<TreeNodePresentation>(replay = 1)
|
||||
private val lastComputedPresentation = AtomicReference<TreeNodePresentation>()
|
||||
val childrenFlow = MutableSharedFlow<List<TreeNodeViewModelImpl>>(replay = 1)
|
||||
private val lastComputedChildren = AtomicReference<List<TreeNodeViewModelImpl>>()
|
||||
|
||||
override fun getUserObject(): Any = domainModel.userObject
|
||||
|
||||
override val presentation: Flow<TreeNodePresentation>
|
||||
get() {
|
||||
ensurePresentationIsLoading()
|
||||
return presentationFlow
|
||||
}
|
||||
|
||||
override val children: Flow<List<TreeNodeViewModel>>
|
||||
get() {
|
||||
ensureChildrenAreLoading()
|
||||
return childrenFlow
|
||||
}
|
||||
|
||||
private fun ensurePresentationIsLoading() {
|
||||
if (presentationLoaded.compareAndSet(false, true)) {
|
||||
updatePresentation()
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureChildrenAreLoading() {
|
||||
if (childrenLoaded.compareAndSet(false, true)) {
|
||||
updateChildren()
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidate(recursive: Boolean) {
|
||||
if (recursive && childrenLoaded.get()) {
|
||||
lastComputedChildren.get()?.forEach { it.invalidate(true) }
|
||||
childrenFlow.resetReplayCache()
|
||||
updateChildren()
|
||||
}
|
||||
if (presentationLoaded.get()) {
|
||||
presentationFlow.resetReplayCache()
|
||||
updatePresentation()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePresentation() {
|
||||
check(presentationUpdateRequests.tryEmit(Unit))
|
||||
}
|
||||
|
||||
fun updateChildren() {
|
||||
check(childrenUpdateRequests.tryEmit(Unit))
|
||||
}
|
||||
|
||||
init {
|
||||
// It's a bit complicated.
|
||||
// If we know whether the node is a leaf or not, we don't need to compute the children to compute the presentation.
|
||||
// If we don't, we need the children, but not their presentations.
|
||||
// Computing the children can be expensive, computing their presentations almost certainly is.
|
||||
// Therefore, for "unsure if leaf" nodes we maintain a separate flow of "raw" children,
|
||||
// which we then use to compute both the presentation and the "real" children (to avoid computing them twice).
|
||||
val unpublishedChildrenFlow: MutableSharedFlow<List<TreeNodeDomainModel>>?
|
||||
|
||||
if (leafStateValue != LeafState.ALWAYS && leafStateValue != LeafState.NEVER) {
|
||||
unpublishedChildrenFlow = MutableSharedFlow<List<TreeNodeDomainModel>>(replay = 1)
|
||||
nodeScope.launch(CoroutineName("Loading children of $this")) {
|
||||
merge(childrenUpdateRequests, presentationUpdateRequests).collectLatest {
|
||||
unpublishedChildrenFlow.emit(domainModel.computeChildren())
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
unpublishedChildrenFlow = null
|
||||
}
|
||||
|
||||
nodeScope.launch(CoroutineName("Value updates of $this")) {
|
||||
if (unpublishedChildrenFlow != null) {
|
||||
presentationUpdateRequests.combine(unpublishedChildrenFlow) { _, children -> children.isEmpty() }.collectLatest { isLeaf ->
|
||||
emitPresentations(isLeaf)
|
||||
}
|
||||
}
|
||||
else {
|
||||
presentationUpdateRequests.collectLatest {
|
||||
emitPresentations(isLeaf = leafStateValue == LeafState.ALWAYS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeScope.launch(CoroutineName("Children updates of $this")) {
|
||||
if (unpublishedChildrenFlow != null) {
|
||||
// This take(1) so this thing is only gets "kick-started" by the first update request,
|
||||
// and then it follows unpublishedChildrenFlow, as that flow itself is updated on the same requests.
|
||||
// But it can also be updated on presentation update requests, and we don't want to start loading children on those.
|
||||
childrenUpdateRequests.take(1).combine(unpublishedChildrenFlow) { _, children -> children }.collectLatest { children ->
|
||||
emitChildren(children)
|
||||
}
|
||||
}
|
||||
else {
|
||||
childrenUpdateRequests.collectLatest {
|
||||
emitChildren(
|
||||
if (leafStateValue == LeafState.ALWAYS) {
|
||||
emptyList()
|
||||
}
|
||||
else {
|
||||
domainModel.computeChildren()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitPresentations(isLeaf: Boolean) {
|
||||
val builder = TreeNodePresentationBuilderImpl(isLeaf)
|
||||
val lastPresentation = lastComputedPresentation.get()
|
||||
// The flow provided by the domain model may cause flickering,
|
||||
// as it's supposed to start from "simple" presentations and then add "heavy" parts.
|
||||
// To avoid this flickering, we only use all provided presentations on the first load,
|
||||
// and then just keep the cached one until the new presentation is computed fully.
|
||||
// It's better to have an outdated presentation than flickering.
|
||||
if (lastPresentation == null) {
|
||||
domainModel.computePresentation(builder).collect { presentation ->
|
||||
lastComputedPresentation.set(presentation)
|
||||
presentationFlow.emit(presentation)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val presentation = domainModel.computePresentation(builder).last()
|
||||
lastComputedPresentation.set(presentation)
|
||||
presentationFlow.emit(presentation)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitChildren(domainChildren: List<TreeNodeDomainModel>) {
|
||||
val children = computeChildren(domainChildren)
|
||||
lastComputedChildren.set(children)
|
||||
childrenFlow.emit(children)
|
||||
}
|
||||
|
||||
private suspend fun computeChildren(domainChildren: List<TreeNodeDomainModel>): List<TreeNodeViewModelImpl> {
|
||||
val oldChildren = lastComputedChildren.get()?.associateBy { it.domainModel } ?: emptyMap()
|
||||
val childViewModels = domainChildren.map { childDomainModel ->
|
||||
val newLeafState = childDomainModel.computeLeafState()
|
||||
val oldChild = oldChildren[childDomainModel]
|
||||
if (oldChild == null || oldChild.leafStateValue != newLeafState) {
|
||||
oldChild?.nodeScope?.cancel()
|
||||
childDomainModel.toViewModel(nodeScope, newLeafState, comparator)
|
||||
}
|
||||
else {
|
||||
oldChild.apply {
|
||||
updatePresentation()
|
||||
}
|
||||
}
|
||||
}
|
||||
return sort(childViewModels)
|
||||
}
|
||||
|
||||
private suspend fun sort(childViewModels: List<TreeNodeViewModelImpl>): List<TreeNodeViewModelImpl> {
|
||||
val comparator = comparator.get()
|
||||
if (comparator == null) return childViewModels
|
||||
val sorted: MutableList<TreeNodeViewModelImpl> = childViewModels.map { child ->
|
||||
nodeScope.async(CoroutineName("Loading to sort: $child")) {
|
||||
if (child.awaitPresentation()) child else null
|
||||
}
|
||||
}.awaitAll().filterNotNull().toMutableList()
|
||||
sorted.sortWith(comparator)
|
||||
return sorted
|
||||
}
|
||||
|
||||
override fun presentationSnapshot(): TreeNodePresentation {
|
||||
val presentationSnapshot = lastComputedPresentation.get()
|
||||
checkNotNull(presentationSnapshot) { "Presentation has not been computed yet" }
|
||||
return presentationSnapshot
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "TreeNodeViewModelImpl@${System.identityHashCode(this)}(" +
|
||||
"domainModel=$domainModel, " +
|
||||
"leafState=$leafStateValue, " +
|
||||
"presentationLoaded=${presentationLoaded.get()}, " +
|
||||
"childrenLoaded=${childrenLoaded.get()}" +
|
||||
")"
|
||||
}
|
||||
|
||||
suspend fun awaitPresentation(): Boolean {
|
||||
val result = nodeScope.launch {
|
||||
presentation.first()
|
||||
}
|
||||
result.join()
|
||||
return !result.isCancelled
|
||||
}
|
||||
}
|
||||
|
||||
private fun TreeNodeDomainModel.toViewModel(
|
||||
parentScope: CoroutineScope,
|
||||
leafState: LeafState,
|
||||
comparator: AtomicReference<Comparator<in TreeNodeViewModel>?>,
|
||||
): TreeNodeViewModelImpl =
|
||||
TreeNodeViewModelImpl(parentScope.childScope(toString()), this, leafState, comparator)
|
||||
|
||||
private suspend inline fun <T> getFlowValue(flow: MutableSharedFlow<T>, allowLoading: Boolean = false): T? =
|
||||
if (allowLoading || flow.replayCache.isNotEmpty()) flow.first() else null
|
||||
|
||||
private val TreePath.node: TreeNodeViewModelImpl
|
||||
get() = lastPathComponent as TreeNodeViewModelImpl
|
||||
@@ -306,6 +306,12 @@
|
||||
serviceImplementation="com.intellij.ui.content.ContentFactoryImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.TreeUIHelper"
|
||||
serviceImplementation="com.intellij.ui.TreeUIHelperImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.treeStructure.TreeDomainModelFactory"
|
||||
serviceImplementation="com.intellij.ui.tree.TreeDomainModelFactoryImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.treeStructure.TreeViewModelFactory"
|
||||
serviceImplementation="com.intellij.ui.tree.TreeViewModelFactoryImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.treeStructure.TreeSwingModelFactory"
|
||||
serviceImplementation="com.intellij.ui.tree.TreeSwingModelFactoryImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.ExpandableItemsHandlerFactory"
|
||||
serviceImplementation="com.intellij.ui.ExpandableItemsHandlerFactoryImpl"/>
|
||||
<applicationService serviceInterface="com.intellij.ui.components.JBHtmlPane$ImplService"
|
||||
|
||||
Reference in New Issue
Block a user