IDEA-370122 Logical Structure: support option "Always select opened element"

GitOrigin-RevId: 8f742fadbc74ebfdfe17dc13bc441c7b1ab1e5ce
This commit is contained in:
Anton Kozub
2025-06-26 18:03:16 +00:00
committed by intellij-monorepo-bot
parent 1159d74523
commit 3bbc30089e
7 changed files with 190 additions and 13 deletions
@@ -2,11 +2,16 @@
package com.intellij.lang.java.logical
import com.intellij.ide.structureView.logical.LogicalStructureElementsProvider
import com.intellij.ide.structureView.logical.model.LogicalPsiDescription
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiElement
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.uast.UClass
import org.jetbrains.uast.toUElement
@ApiStatus.Internal
class ClassOwnerLogicalStructureElementsProvider: LogicalStructureElementsProvider<PsiClassOwner, Any> {
class ClassOwnerLogicalStructureElementsProvider: LogicalStructureElementsProvider<PsiClassOwner, Any>, LogicalPsiDescription {
override fun getElements(parent: PsiClassOwner): List<Any> {
val result = mutableListOf<Any>()
var convertedAtLeastOne = false
@@ -27,4 +32,13 @@ class ClassOwnerLogicalStructureElementsProvider: LogicalStructureElementsProvid
if (convertedAtLeastOne) return result
return emptyList()
}
override fun getSuitableElement(psiElement: PsiElement): PsiElement? {
if (psiElement is PsiClass) return psiElement
return (psiElement.toUElement() as? UClass)?.javaPsi
}
override fun isAskChildren(): Boolean {
return true
}
}
@@ -5,19 +5,45 @@ import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.structureView.impl.common.PsiTreeElementBase
import com.intellij.ide.structureView.logical.impl.LogicalStructureViewService
import com.intellij.ide.structureView.newStructureView.StructureViewComponent
import com.intellij.ide.util.treeView.PresentableNodeDescriptor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.ui.SimpleTextAttributes
import junit.framework.AssertionFailedError
import junit.framework.ComparisonFailure
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import javax.swing.Icon
private const val MAX_DEPTH = 20
fun assertLogicalStructure(
psiFile: PsiFile,
expectedStructureInitializer: LogicalStructureNode.() -> Unit,
) {
assertLogicalStructure(psiFile, null, null, expectedStructureInitializer)
}
fun assertLogicalStructure(
psiFile: PsiFile,
nodePath: String,
expectedStructureInitializer: LogicalStructureNode.() -> Unit,
) {
assertLogicalStructure(psiFile, null, nodePath, expectedStructureInitializer)
}
/**
* @param psiFile - is a file which will be created the structure for
* @param selectedElement - selected psiElement in the file.
* If not null then the corresponding logical element will be found and compared with expected
* @param nodePath - path to subtree if one wants to check not the whole structure, but only some branch
* @param expectedStructureInitializer - the builder for the expected structure
*/
fun assertLogicalStructure(
psiFile: PsiFile,
selectedElement: PsiElement? = null,
nodePath: String? = null,
expectedStructureInitializer: LogicalStructureNode.() -> Unit,
) {
@@ -48,6 +74,24 @@ fun assertLogicalStructure(
expectedRoot.print("", false),
actualRoot.print("", false))
}
if (selectedElement != null) {
val expectedSelectedPath = expectedRoot.getSelectedNodePath().let { it.subList(1, it.size) }
val select = (structureView as StructureViewComponent).select(selectedElement, true)
val actualPaths = PlatformTestUtil.waitForPromise(select)!!.path.toList().let { it.subList(1, it.size) }
val nodePaths = nodePath?.split("/") ?: emptyList()
for ((index, any) in actualPaths.withIndex()) {
if (index < nodePaths.size) {
assertTrue("Selected node is different: ", any.toString().startsWith(nodePaths[index]))
}
else {
val expectedNode = expectedSelectedPath.getOrNull(index - nodePaths.size)!!
val expectedName = if (expectedNode.coloredTextElements.isNotEmpty()) {
expectedNode.coloredTextElements.joinToString("") { it.text }
} else expectedNode.name
assertEquals("Selected node is different: ", expectedName, any.toString())
}
}
}
}
private fun createActualNode(element: StructureViewTreeElement): LogicalStructureNode {
@@ -77,6 +121,7 @@ class LogicalStructureNode(
private var childrenDontMatter = false
private var childrenOrderDontMatter = false
private var navigationElementSupplier: (() -> PsiElement?)? = null
private var isSelected = false
fun subNode(subNode: LogicalStructureNode) {
subNodes.add(subNode)
@@ -103,6 +148,10 @@ class LogicalStructureNode(
childrenOrderDontMatter = true
}
fun selected() {
isSelected = true
}
fun navigationElement(element: PsiElement) {
navigationElementSupplier = { element }
}
@@ -179,4 +228,17 @@ class LogicalStructureNode(
return result
}
fun getSelectedNodePath(): List<LogicalStructureNode> {
if (isSelected) {
return listOf(this)
}
for (subNode in subNodes) {
val selectedPath = subNode.getSelectedNodePath()
if (selectedPath.isNotEmpty()) {
return listOf(this) + selectedPath
}
}
return emptyList()
}
}
@@ -10,6 +10,7 @@ import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
import com.intellij.ide.structureView.logical.LogicalStructureDataKeys;
import com.intellij.ide.structureView.logical.impl.LogicalStructureViewModel;
import com.intellij.ide.structureView.logical.impl.LogicalStructureViewTreeElement;
import com.intellij.ide.structureView.logical.model.LogicalPsiDescription;
import com.intellij.ide.structureView.symbol.DelegatingPsiElementWithSymbolPointer;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.ide.ui.customization.CustomizationUtil;
@@ -451,6 +452,9 @@ public class StructureViewComponent extends SimpleToolWindowPanel implements Tre
AsyncPromise<TreePath> result = myCurrentFocusPromise = new AsyncPromise<>();
var state = new StructureViewSelectVisitorState();
TreeVisitor visitor = new TreeVisitor() {
private Set<LogicalPsiDescription> psiDescriptions = null;
@Override
public @NotNull TreeVisitor.VisitThread visitThread() {
return VisitThread.BGT;
@@ -462,6 +466,14 @@ public class StructureViewComponent extends SimpleToolWindowPanel implements Tre
result.setError("rejected");
return TreeVisitor.Action.INTERRUPT;
}
if (myTreeModel instanceof LogicalStructureViewModel logicalStructureViewModel) {
StructureViewTreeElement treeElement = getStructureTreeElement(path.getLastPathComponent());
if (treeElement == null) return TreeVisitor.Action.CONTINUE;
if (psiDescriptions == null) {
psiDescriptions = logicalStructureViewModel.getAssembledModel().getLogicalPsiDescriptions();
}
return logicalStructureViewModel.visitPathForLogicalElementSelection(treeElement, element, psiDescriptions);
}
return visitPathForElementSelection(path, element, editorOffset, state);
}
};
@@ -560,7 +572,7 @@ public class StructureViewComponent extends SimpleToolWindowPanel implements Tre
return;
}
if (!getSettings().AUTOSCROLL_FROM_SOURCE) {
if (!isShowing() || !getSettings().AUTOSCROLL_FROM_SOURCE) {
return;
}
@@ -23,10 +23,14 @@
- isAutoExpand(java.lang.Object):Z
*f:com.intellij.ide.structureView.logical.model.LogicalModelPresentationProvider$Companion
- f:getForObject(java.lang.Object):com.intellij.ide.structureView.logical.model.LogicalModelPresentationProvider
*:com.intellij.ide.structureView.logical.model.LogicalPsiDescription
- a:getSuitableElement(com.intellij.psi.PsiElement):com.intellij.psi.PsiElement
- isAskChildren():Z
*f:com.intellij.ide.structureView.logical.model.LogicalStructureAssembledModel
- *sf:Companion:com.intellij.ide.structureView.logical.model.LogicalStructureAssembledModel$Companion
- equals(java.lang.Object):Z
- f:getChildren():java.util.List
- f:getLogicalPsiDescriptions():java.util.Set
- f:getModel():java.lang.Object
- f:getParent():com.intellij.ide.structureView.logical.model.LogicalStructureAssembledModel
- f:getProject():com.intellij.openapi.project.Project
@@ -3,21 +3,13 @@ package com.intellij.ide.structureView.logical.impl
import com.intellij.ide.TypePresentationService
import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewClickEvent
import com.intellij.ide.structureView.StructureViewEventsCollector
import com.intellij.ide.structureView.StructureViewModel
import com.intellij.ide.structureView.StructureViewModelBase
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.structureView.*
import com.intellij.ide.structureView.impl.common.PsiTreeElementBase
import com.intellij.ide.structureView.logical.ContainerElementsProvider
import com.intellij.ide.structureView.logical.ExternalElementsProvider
import com.intellij.ide.structureView.logical.LogicalStructureTreeElementProvider
import com.intellij.ide.structureView.logical.PropertyElementProvider
import com.intellij.ide.structureView.logical.model.LogicalContainerPresentationProvider
import com.intellij.ide.structureView.logical.model.LogicalModelPresentationProvider
import com.intellij.ide.structureView.logical.model.LogicalContainer
import com.intellij.ide.structureView.logical.model.LogicalStructureAssembledModel
import com.intellij.ide.structureView.logical.model.ProvidedLogicalContainer
import com.intellij.ide.structureView.logical.model.*
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.editor.Editor
@@ -25,13 +17,14 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiTarget
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.tree.TreeVisitor
import org.jetbrains.annotations.ApiStatus
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import javax.swing.Icon
@ApiStatus.Internal
class LogicalStructureViewModel private constructor(psiFile: PsiFile, editor: Editor?, assembledModel: LogicalStructureAssembledModel<*>, elementBuilder: ElementsBuilder)
class LogicalStructureViewModel private constructor(psiFile: PsiFile, editor: Editor?, val assembledModel: LogicalStructureAssembledModel<*>, elementBuilder: ElementsBuilder)
: StructureViewModelBase(psiFile, editor, elementBuilder.createViewTreeElement(assembledModel)),
StructureViewModel.ElementInfoProvider, StructureViewModel.ExpandInfoProvider, StructureViewModel.ClickHandler {
@@ -68,6 +61,42 @@ class LogicalStructureViewModel private constructor(psiFile: PsiFile, editor: Ed
}
}
override fun findAcceptableElement(element: PsiElement?): Any? {
var elementTmp = element ?: return null
val psiDescriptions = assembledModel.getLogicalPsiDescriptions()
while (elementTmp !is PsiFile) {
for (description in psiDescriptions) {
val suitableElement = description.getSuitableElement(elementTmp)
if (suitableElement != null) return suitableElement
}
elementTmp = elementTmp.getParent() ?: return null
}
return null
}
fun visitPathForLogicalElementSelection(treeElement: StructureViewTreeElement, element: Any?, psiDescriptions: Set<LogicalPsiDescription>): TreeVisitor.Action {
if (element !is PsiElement) return TreeVisitor.Action.SKIP_CHILDREN
if (treeElement is ElementsBuilder.LogicalGroupStructureElement<*>) {
if (treeElement.grouper is ExternalElementsProvider<*, *>) {
return TreeVisitor.Action.SKIP_CHILDREN
}
return TreeVisitor.Action.CONTINUE
}
val targetElement = psiDescriptions.firstNotNullOfOrNull {
it.getSuitableElement(element)
} ?: return TreeVisitor.Action.SKIP_CHILDREN
if (treeElement is ElementsBuilder.PsiElementStructureElement<*>) {
if (treeElement.element == targetElement) {
return TreeVisitor.Action.INTERRUPT
}
else if (treeElement.element?.containingFile != targetElement.containingFile) {
return TreeVisitor.Action.SKIP_CHILDREN
}
return TreeVisitor.Action.CONTINUE
}
return TreeVisitor.Action.SKIP_CHILDREN
}
private fun getModel(element: StructureViewTreeElement): Any? {
return when (element) {
is ElementsBuilder.LogicalGroupStructureElement<*> -> element.grouper
@@ -0,0 +1,25 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.structureView.logical.model
import com.intellij.psi.PsiElement
import org.jetbrains.annotations.ApiStatus
/**
* Interface for logical elements which allow them provide information which psi elements they can represent
*/
@ApiStatus.Experimental
interface LogicalPsiDescription {
/**
* psi element which can be represented by this model or its children
* @return null - if elements with this type are not supported
*/
fun getSuitableElement(psiElement: PsiElement): PsiElement?
/**
* false - if this logical element can provide info about its children
* true - if one needs to go deeper to collect full information
*/
fun isAskChildren(): Boolean = false
}
@@ -43,6 +43,37 @@ class LogicalStructureAssembledModel<T> private constructor(
return result
}
fun getLogicalPsiDescriptions(): Set<LogicalPsiDescription> {
return model?.let { getLogicalPsiDescriptions(it) } ?: emptySet()
}
private fun getLogicalPsiDescriptions(model: Any): Set<LogicalPsiDescription> {
val result = mutableSetOf<LogicalPsiDescription>()
if (model is LogicalPsiDescription) {
if (!model.isAskChildren()) {
return setOf(model)
}
else {
result.add(model)
}
}
for (provider in LogicalStructureElementsProvider.getProviders(model)) {
if (provider is LogicalPsiDescription) {
if (!provider.isAskChildren()) {
return setOf(provider)
}
else {
result.add(provider)
}
}
if (provider is ExternalElementsProvider<*, *>) continue
provider.getElements(model).forEach { child ->
result.addAll(getLogicalPsiDescriptions(child))
}
}
return result
}
internal fun hasSameModelParent(): Boolean {
var parentTmp = parent
while (parentTmp != null) {