diff --git a/java/java-structure-view/src/com/intellij/lang/java/logical/ClassOwnerLogicalStructureElementsProvider.kt b/java/java-structure-view/src/com/intellij/lang/java/logical/ClassOwnerLogicalStructureElementsProvider.kt index df16bbe3d43f..82e04034d54c 100644 --- a/java/java-structure-view/src/com/intellij/lang/java/logical/ClassOwnerLogicalStructureElementsProvider.kt +++ b/java/java-structure-view/src/com/intellij/lang/java/logical/ClassOwnerLogicalStructureElementsProvider.kt @@ -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 { +class ClassOwnerLogicalStructureElementsProvider: LogicalStructureElementsProvider, LogicalPsiDescription { override fun getElements(parent: PsiClassOwner): List { val result = mutableListOf() 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 + } } diff --git a/java/testFramework/src/com/intellij/testFramework/LogicalStructureTestUtils.kt b/java/testFramework/src/com/intellij/testFramework/LogicalStructureTestUtils.kt index d382e095be7a..d007d1118514 100644 --- a/java/testFramework/src/com/intellij/testFramework/LogicalStructureTestUtils.kt +++ b/java/testFramework/src/com/intellij/testFramework/LogicalStructureTestUtils.kt @@ -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 { + if (isSelected) { + return listOf(this) + } + for (subNode in subNodes) { + val selectedPath = subNode.getSelectedNodePath() + if (selectedPath.isNotEmpty()) { + return listOf(this) + selectedPath + } + } + return emptyList() + } + } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/ide/structureView/newStructureView/StructureViewComponent.java b/platform/lang-impl/src/com/intellij/ide/structureView/newStructureView/StructureViewComponent.java index 4b08ce5ce3d8..c71c29c3fe52 100644 --- a/platform/lang-impl/src/com/intellij/ide/structureView/newStructureView/StructureViewComponent.java +++ b/platform/lang-impl/src/com/intellij/ide/structureView/newStructureView/StructureViewComponent.java @@ -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 result = myCurrentFocusPromise = new AsyncPromise<>(); var state = new StructureViewSelectVisitorState(); TreeVisitor visitor = new TreeVisitor() { + + private Set 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; } diff --git a/platform/structure-view-impl/api-dump-experimental.txt b/platform/structure-view-impl/api-dump-experimental.txt index 682adf6e7e59..53b3d23c2f55 100644 --- a/platform/structure-view-impl/api-dump-experimental.txt +++ b/platform/structure-view-impl/api-dump-experimental.txt @@ -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 diff --git a/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/impl/LogicalStructureViewModel.kt b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/impl/LogicalStructureViewModel.kt index 1ac39a4f0845..9239c333913f 100644 --- a/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/impl/LogicalStructureViewModel.kt +++ b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/impl/LogicalStructureViewModel.kt @@ -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): 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 diff --git a/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalPsiDescription.kt b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalPsiDescription.kt new file mode 100644 index 000000000000..57ddbabf3697 --- /dev/null +++ b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalPsiDescription.kt @@ -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 + +} \ No newline at end of file diff --git a/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalStructureAssembledModel.kt b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalStructureAssembledModel.kt index ba5f3c5dbd97..bd092f875ae5 100644 --- a/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalStructureAssembledModel.kt +++ b/platform/structure-view-impl/src/com/intellij/ide/structureView/logical/model/LogicalStructureAssembledModel.kt @@ -43,6 +43,37 @@ class LogicalStructureAssembledModel private constructor( return result } + fun getLogicalPsiDescriptions(): Set { + return model?.let { getLogicalPsiDescriptions(it) } ?: emptySet() + } + + private fun getLogicalPsiDescriptions(model: Any): Set { + val result = mutableSetOf() + 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) {