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:
Sergei Tachenov
2024-09-13 16:39:29 +03:00
committed by intellij-monorepo-bot
parent 1f3bbf9580
commit be921b067e
18 changed files with 1426 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)"
}

View File

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

View File

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

View File

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

View File

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

View File

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