From 593a8014a48194d2a23479bbd09d230efc2b5b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20Gy=C3=B6ri?= Date: Fri, 12 Aug 2022 00:49:13 -0700 Subject: [PATCH] Golang: Display Go external libraries based on importpath structure (bazelbuild/intellij PR import #2973) This is a modified version of the original change by Brandon Lico with a few modifications: * read from the fileToImportPathMap directly when modifying the structure * add unit tests. * We don't inherit from BlazeExternalSyntheticLibrary since that contains irrelevant logic. Also we can't logically fulfill the "Only supports one instance per value of presentableText." condition that's outlined in the class javadoc since we could have nodes with identical presentableText properties in the tree in different positions. Issue number: #1744 #2365 Modifies the structure of the external "Go Libraries" project view to be based on importpath rather than a flat list of files. See my comment within #2365 for an example of how the new structure looks. Closes https://github.com/bazelbuild/intellij/pull/2973 Unit tests added. (cherry picked from commit 4d0f56bfdc193ceeae04da094c48008c60825689) --- .../BlazeExternalSyntheticLibrary.java | 2 +- golang/BUILD | 4 +- golang/src/META-INF/go-contents.xml | 1 + .../golang/resolve/BlazeGoPackageFactory.java | 5 +- ...BlazeGoAdditionalLibraryRootsProvider.java | 4 +- .../BlazeGoExternalSyntheticLibrary.java | 71 +++ .../BlazeGoTreeStructureProvider.java | 187 ++++++++ .../GoSyntheticLibraryElementNode.java | 91 ++++ .../BlazeGoTreeStructureProviderTest.java | 416 ++++++++++++++++++ 9 files changed, 776 insertions(+), 5 deletions(-) create mode 100644 golang/src/com/google/idea/blaze/golang/treeview/BlazeGoExternalSyntheticLibrary.java create mode 100644 golang/src/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProvider.java create mode 100644 golang/src/com/google/idea/blaze/golang/treeview/GoSyntheticLibraryElementNode.java create mode 100644 golang/tests/unittests/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProviderTest.java diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeExternalSyntheticLibrary.java b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeExternalSyntheticLibrary.java index 33eb7e55fc9..4e6ff0f89ab 100644 --- a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeExternalSyntheticLibrary.java +++ b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeExternalSyntheticLibrary.java @@ -49,7 +49,7 @@ public final class BlazeExternalSyntheticLibrary extends SyntheticLibrary * equals, hashcode -- there must only be one instance per value of this text * @param files collection of files that this synthetic library is responsible for. */ - BlazeExternalSyntheticLibrary(String presentableText, Collection files) { + public BlazeExternalSyntheticLibrary(String presentableText, Collection files) { this.presentableText = presentableText; this.files = ImmutableSet.copyOf(files); this.validFiles = diff --git a/golang/BUILD b/golang/BUILD index aee8b3e00f0..9bca031f3e5 100644 --- a/golang/BUILD +++ b/golang/BUILD @@ -71,10 +71,12 @@ intellij_unit_test_suite( ":golang", "//base", "//base:unit_test_utils", + "//common/experiments", + "//common/experiments:unit_test_utils", "//intellij_platform_sdk:jsr305", "//intellij_platform_sdk:plugin_api_for_tests", "//intellij_platform_sdk:test_libs", - "//sdkcompat", + "//testing:lib", "//third_party/go:go_for_tests", "@junit//jar", ], diff --git a/golang/src/META-INF/go-contents.xml b/golang/src/META-INF/go-contents.xml index 405669fae6e..97c0bae07fd 100644 --- a/golang/src/META-INF/go-contents.xml +++ b/golang/src/META-INF/go-contents.xml @@ -44,5 +44,6 @@ + diff --git a/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoPackageFactory.java b/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoPackageFactory.java index fb2aca02495..175ee05887b 100644 --- a/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoPackageFactory.java +++ b/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoPackageFactory.java @@ -36,7 +36,8 @@ import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; -class BlazeGoPackageFactory implements GoPackageFactory { +/** Updates and exposes a map of import paths to files. */ +public class BlazeGoPackageFactory implements GoPackageFactory { @Nullable @Override public GoPackage createPackage(GoFile goFile) { @@ -54,7 +55,7 @@ public GoPackage createPackage(GoFile goFile) { } @Nullable - static ConcurrentMap getFileToImportPathMap(Project project) { + public static ConcurrentMap getFileToImportPathMap(Project project) { return SyncCache.getInstance(project) .get(BlazeGoPackageFactory.class, BlazeGoPackageFactory::buildFileToImportPathMap); } diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoAdditionalLibraryRootsProvider.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoAdditionalLibraryRootsProvider.java index f8435392e4c..fe55cb59fa4 100644 --- a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoAdditionalLibraryRootsProvider.java +++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoAdditionalLibraryRootsProvider.java @@ -32,9 +32,11 @@ /** Provides out-of-project go sources for indexing. */ public final class BlazeGoAdditionalLibraryRootsProvider extends BlazeExternalLibraryProvider { + public static final String GO_EXTERNAL_LIBRARY_ROOT_NAME = "Go Libraries"; + @Override protected String getLibraryName() { - return "Go Libraries"; + return GO_EXTERNAL_LIBRARY_ROOT_NAME; } @Override diff --git a/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoExternalSyntheticLibrary.java b/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoExternalSyntheticLibrary.java new file mode 100644 index 00000000000..c1252cb1c0e --- /dev/null +++ b/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoExternalSyntheticLibrary.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.idea.blaze.golang.treeview; + +import com.google.common.collect.ImmutableSet; +import com.intellij.navigation.ItemPresentation; +import com.intellij.openapi.roots.SyntheticLibrary; +import com.intellij.openapi.vfs.VirtualFile; +import icons.BlazeIcons; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.swing.Icon; + +/** Represents a {@link SyntheticLibrary} with a mutable set of child files. */ +final class BlazeGoExternalSyntheticLibrary extends SyntheticLibrary implements ItemPresentation { + + private final SortedSet childFiles; + private final String presentableText; + + BlazeGoExternalSyntheticLibrary(String presentableText, ImmutableSet childFiles) { + this.childFiles = new TreeSet<>(Comparator.comparing(VirtualFile::toString)); + this.childFiles.addAll(childFiles); + this.presentableText = presentableText; + } + + void addFiles(ImmutableSet files) { + childFiles.addAll(files); + } + + @Override + public String getPresentableText() { + return presentableText; + } + + @Override + public Icon getIcon(boolean unused) { + return BlazeIcons.Logo; + } + + @Override + public SortedSet getSourceRoots() { + return childFiles; + } + + @Override + public boolean equals(Object o) { + return o instanceof BlazeGoExternalSyntheticLibrary + && ((BlazeGoExternalSyntheticLibrary) o).presentableText.equals(presentableText) + && ((BlazeGoExternalSyntheticLibrary) o).getSourceRoots().equals(getSourceRoots()); + } + + @Override + public int hashCode() { + return presentableText.hashCode(); + } +} diff --git a/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProvider.java b/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProvider.java new file mode 100644 index 00000000000..5d2166983ac --- /dev/null +++ b/golang/src/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProvider.java @@ -0,0 +1,187 @@ +/* + * Copyright 2022 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.idea.blaze.golang.treeview; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.idea.blaze.golang.sync.BlazeGoAdditionalLibraryRootsProvider.GO_EXTERNAL_LIBRARY_ROOT_NAME; + +import com.google.common.base.Functions; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import com.google.idea.blaze.base.settings.Blaze; +import com.google.idea.blaze.golang.resolve.BlazeGoPackageFactory; +import com.google.idea.common.experiments.FeatureRolloutExperiment; +import com.intellij.ide.projectView.TreeStructureProvider; +import com.intellij.ide.projectView.ViewSettings; +import com.intellij.ide.projectView.impl.nodes.PsiFileNode; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.Function; + +/** + * Modifies the project view by replacing the External Go Libraries root node (containing a flat + * list of sources) with a root node that structures sources based on their import paths. + */ +public final class BlazeGoTreeStructureProvider implements TreeStructureProvider, DumbAware { + + public static final FeatureRolloutExperiment experiment = + new FeatureRolloutExperiment("go.import.treestructure"); + + @Override + public Collection> modify( + AbstractTreeNode parent, Collection> children, ViewSettings settings) { + Project project = parent.getProject(); + if (!Blaze.isBlazeProject(project) + || !experiment.isEnabled() + || !isGoBlazeExternalLibraryRoot(parent)) { + return children; + } + ImmutableMap> originalFileNodes = + getOriginalFileNodesMap(children); + + Map fileToImportPathMap = BlazeGoPackageFactory.getFileToImportPathMap(project); + if (fileToImportPathMap == null) { + return children; + } + + SortedMap> newChildren = new TreeMap<>(); + + ImmutableListMultimap importPathToFilesMap = + originalFileNodes.keySet().stream() + .collect( + ImmutableListMultimap.toImmutableListMultimap( + // Some nodes may, for some reason, not be in importPathMap, use the empty + // String as a guard character. + virtualFile -> + fileToImportPathMap.getOrDefault(VfsUtil.virtualToIoFile(virtualFile), ""), + Function.identity())); + + for (String importPath : importPathToFilesMap.keySet()) { + if (importPath.isEmpty()) { + continue; + } + generateTree( + settings, + project, + newChildren, + importPath, + ImmutableSet.copyOf(importPathToFilesMap.get(importPath)), + originalFileNodes); + } + return Streams.concat( + newChildren.values().stream(), + // Put nodes without an importPath as direct children. + importPathToFilesMap.get("").stream().map(originalFileNodes::get)) + .collect(toImmutableList()); + } + + private static void generateTree( + ViewSettings settings, + Project project, + SortedMap> newChildren, + String importPath, + ImmutableSet availableFiles, + Map> originalFileNodes) { + Iterator pathIter = Paths.get(importPath).iterator(); + if (!pathIter.hasNext()) { + return; + } + // Root nodes (e.g., src) have to be added directly to newChildren. + String rootName = pathIter.next().toString(); + GoSyntheticLibraryElementNode root = + (GoSyntheticLibraryElementNode) + newChildren.computeIfAbsent( + rootName, + (unused) -> + new GoSyntheticLibraryElementNode( + project, + new BlazeGoExternalSyntheticLibrary(rootName, availableFiles), + rootName, + settings, + new TreeMap<>())); + + // Child nodes (e.g., package_name under src) are added under root nodes recursively. + GoSyntheticLibraryElementNode leaf = + buildChildTree(settings, project, availableFiles, pathIter, root); + + // Required for files to actually show up in the Project View. + availableFiles.forEach((file) -> leaf.addChild(file.getName(), originalFileNodes.get(file))); + } + + /** + * Recurse down the import path tree and add elements as children. + * + *

Fills previously created nodes with source files from the current import path. + */ + private static GoSyntheticLibraryElementNode buildChildTree( + ViewSettings settings, + Project project, + ImmutableSet files, + Iterator pathIter, + GoSyntheticLibraryElementNode parent) { + while (pathIter.hasNext()) { + parent.addFiles(files); + String dirName = pathIter.next().toString(); + + // current path already was created, no need to re-create + if (parent.hasChild(dirName)) { + parent = parent.getChildNode(dirName); + continue; + } + GoSyntheticLibraryElementNode libraryNode = + new GoSyntheticLibraryElementNode( + project, + new BlazeGoExternalSyntheticLibrary(dirName, files), + dirName, + settings, + new TreeMap<>()); + parent.addChild(dirName, libraryNode); + parent = libraryNode; + } + return parent; + } + + private static boolean isGoBlazeExternalLibraryRoot(AbstractTreeNode parent) { + if (parent.getName() == null) { + return false; + } + return parent.getName().equals(GO_EXTERNAL_LIBRARY_ROOT_NAME); + } + + private static ImmutableMap> getOriginalFileNodesMap( + Collection> children) { + + return children.stream() + .filter(child -> (child instanceof PsiFileNode)) + .filter(child -> ((PsiFileNode) child).getVirtualFile() != null) + .collect( + toImmutableMap(child -> ((PsiFileNode) child).getVirtualFile(), Functions.identity())); + } +} diff --git a/golang/src/com/google/idea/blaze/golang/treeview/GoSyntheticLibraryElementNode.java b/golang/src/com/google/idea/blaze/golang/treeview/GoSyntheticLibraryElementNode.java new file mode 100644 index 00000000000..a9260def6b7 --- /dev/null +++ b/golang/src/com/google/idea/blaze/golang/treeview/GoSyntheticLibraryElementNode.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.idea.blaze.golang.treeview; + +import com.goide.GoIcons; +import com.google.common.collect.ImmutableSet; +import com.intellij.ide.projectView.ViewSettings; +import com.intellij.ide.projectView.impl.nodes.SyntheticLibraryElementNode; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.navigation.ItemPresentation; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import java.util.Collection; +import java.util.Map; +import java.util.SortedMap; +import javax.swing.Icon; + +/** + * Represents a Go directory node within the "External Libraries" project view. + * + *

It can have an arbitrary amount of child nodes, forming a tree. Child nodes may be added and + * queried with the appropriate methods. + */ +class GoSyntheticLibraryElementNode extends SyntheticLibraryElementNode { + private final Map> children; + + GoSyntheticLibraryElementNode( + Project project, + BlazeGoExternalSyntheticLibrary library, + String dirName, + ViewSettings settings, + SortedMap> children) { + + super(project, library, new BlazeGoLibraryItemPresentation(dirName), settings); + this.children = children; + } + + @Override + public Collection> getChildren() { + return children.values(); + } + + GoSyntheticLibraryElementNode getChildNode(String dirName) { + return (GoSyntheticLibraryElementNode) children.get(dirName); + } + + public void addChild(String dirName, AbstractTreeNode child) { + children.put(dirName, child); + } + + public boolean hasChild(String dirName) { + return children.containsKey(dirName); + } + + public void addFiles(ImmutableSet files) { + ((BlazeGoExternalSyntheticLibrary) getValue()).addFiles(files); + } + + private static class BlazeGoLibraryItemPresentation implements ItemPresentation { + + private final String dirName; + + public BlazeGoLibraryItemPresentation(String dirName) { + this.dirName = dirName; + } + + @Override + public String getPresentableText() { + return dirName; + } + + @Override + public Icon getIcon(boolean ignore) { + return GoIcons.PACKAGE; + } + } +} diff --git a/golang/tests/unittests/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProviderTest.java b/golang/tests/unittests/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProviderTest.java new file mode 100644 index 00000000000..75cc4fbbd7a --- /dev/null +++ b/golang/tests/unittests/com/google/idea/blaze/golang/treeview/BlazeGoTreeStructureProviderTest.java @@ -0,0 +1,416 @@ +/* + * Copyright 2022 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.idea.blaze.golang.treeview; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.idea.blaze.golang.sync.BlazeGoAdditionalLibraryRootsProvider.GO_EXTERNAL_LIBRARY_ROOT_NAME; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.idea.blaze.base.io.VirtualFileSystemProvider; +import com.google.idea.blaze.base.settings.BlazeImportSettings; +import com.google.idea.blaze.base.settings.BlazeImportSettingsManager; +import com.google.idea.blaze.base.settings.BuildSystemName; +import com.google.idea.blaze.base.sync.SyncCache; +import com.google.idea.blaze.base.sync.libraries.BlazeExternalSyntheticLibrary; +import com.google.idea.blaze.golang.resolve.BlazeGoPackageFactory; +import com.google.idea.common.experiments.ExperimentService; +import com.google.idea.common.experiments.MockExperimentService; +import com.google.idea.testing.IntellijRule; +import com.intellij.ide.projectView.ViewSettings; +import com.intellij.ide.projectView.impl.nodes.PsiFileNode; +import com.intellij.ide.projectView.impl.nodes.SyntheticLibraryElementNode; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.ide.util.treeView.TreeAnchorizer; +import com.intellij.lang.FileASTNode; +import com.intellij.mock.MockLocalFileSystem; +import com.intellij.mock.MockVirtualFile; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileSystemItem; +import com.intellij.psi.PsiManager; +import com.intellij.psi.impl.FakePsiElement; +import com.intellij.psi.search.PsiElementProcessor; +import java.io.File; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link BlazeGoTreeStructureProvider} */ +@RunWith(JUnit4.class) +public class BlazeGoTreeStructureProviderTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Rule public IntellijRule intellij = new IntellijRule(); + @Rule public TemporaryFolder folder = new TemporaryFolder(); + private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS = + new BlazeImportSettings("", "", "", "", BuildSystemName.Blaze); + @Mock private SyncCache syncCache; + + private ConcurrentHashMap fileToImportPathMap; + private SyntheticLibraryElementNode rootNode; + + @Before + public void setUp() { + intellij.registerApplicationService(TreeAnchorizer.class, new FakeTreeAnchorizer()); + + intellij.registerProjectService( + BlazeImportSettingsManager.class, new BlazeImportSettingsManager(intellij.getProject())); + + MockExperimentService experimentService = new MockExperimentService(); + intellij.registerApplicationService(ExperimentService.class, experimentService); + experimentService.setFeatureRolloutExperiment(BlazeGoTreeStructureProvider.experiment, 100); + + intellij.registerApplicationService(VirtualFileSystemProvider.class, MockLocalFileSystem::new); + intellij.registerProjectService(SyncCache.class, syncCache); + + fileToImportPathMap = new ConcurrentHashMap<>(); + when(syncCache.get(eq(BlazeGoPackageFactory.class), any())).thenReturn(fileToImportPathMap); + + BlazeImportSettingsManager.getInstance(intellij.getProject()) + .setImportSettings(DUMMY_IMPORT_SETTINGS); + + rootNode = createRootNode(GO_EXTERNAL_LIBRARY_ROOT_NAME); + } + + @Test + public void givenDifferentImportPaths_createTwoRootNodes() { + // GIVEN + String fileName1 = "bar.go"; + VirtualFile file1 = MockVirtualFile.file(fileName1); + PsiFileNode fileNode1 = createPsiFileNode(file1); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file1), "root1"); + + String fileName2 = "buzz.go"; + VirtualFile file2 = MockVirtualFile.file(fileName2); + PsiFileNode fileNode2 = createPsiFileNode(file2); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file2), "root2"); + + // WHEN + ImmutableList actualChildren = + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(fileNode1, fileNode2), + /* settings= */ new ViewSettings() {}) + .stream() + .map(GoSyntheticLibraryElementNode.class::cast) + .collect(toImmutableList()); + + // THEN + assertThat(actualChildren).hasSize(2); + assertThat(actualChildren.get(0).getName()).isEqualTo("root1"); + assertThat(actualChildren.get(1).getName()).isEqualTo("root2"); + + ImmutableList> secondLevelImportPath1 = + ImmutableList.copyOf(actualChildren.get(0).getChildren()); + assertThat(secondLevelImportPath1).containsExactly(fileNode1); + + ImmutableList> secondLevelImportPath2 = + ImmutableList.copyOf(actualChildren.get(1).getChildren()); + assertThat(secondLevelImportPath2).containsExactly(fileNode2); + } + + @Test + public void givenNestedImportPaths_createMultiLevelStructure() { + // GIVEN + String fileName1 = "bar.go"; + VirtualFile file1 = MockVirtualFile.file(fileName1); + PsiFileNode fileNode1 = createPsiFileNode(file1); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file1), "root"); + + String fileName2 = "buzz.go"; + VirtualFile file2 = MockVirtualFile.file(fileName2); + PsiFileNode fileNode2 = createPsiFileNode(file2); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file2), "root/someOtherFolder"); + + // WHEN + ImmutableList actualChildren = + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(fileNode1, fileNode2), + /* settings= */ new ViewSettings() {}) + .stream() + .map(GoSyntheticLibraryElementNode.class::cast) + .collect(toImmutableList()); + + // THEN + assertThat(actualChildren).hasSize(1); + assertThat(actualChildren.get(0).getName()).isEqualTo("root"); + + ImmutableList> secondLevel = + ImmutableList.copyOf(actualChildren.get(0).getChildren()); + assertThat(secondLevel).hasSize(2); + assertThat(secondLevel.get(0)).isEqualTo(fileNode1); + assertThat(secondLevel.get(1).getName()).isEqualTo("someOtherFolder"); + + ImmutableList> thirdLevel = + ImmutableList.copyOf(secondLevel.get(1).getChildren()); + assertThat(thirdLevel).containsExactly(fileNode2); + } + + @Test + public void givenFilesNotInToImportPathMap_attachAtRootLevel() { + String fileName1 = "bar.go"; + VirtualFile file1 = MockVirtualFile.file(fileName1); + PsiFileNode fileNode1 = createPsiFileNode(file1); + + String fileName2 = "bar.go"; + VirtualFile file2 = MockVirtualFile.file(fileName2); + PsiFileNode fileNode2 = createPsiFileNode(file2); + + ImmutableList> actualChildren = + ImmutableList.copyOf( + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(fileNode1, fileNode2), + /* settings= */ new ViewSettings() {})); + + assertThat(actualChildren).containsExactly(fileNode1, fileNode2); + } + + @Test + public void givenSingleNode_addSourceRootsToLibrary() { + String fileName = "bar.go"; + VirtualFile file = MockVirtualFile.file(fileName); + PsiFileNode fileNode = createPsiFileNode(file); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file), "root"); + + ImmutableList> actualChildren = + ImmutableList.copyOf( + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(fileNode), + /* settings= */ new ViewSettings() {})); + + assertThat(actualChildren).hasSize(1); + assertThat(actualChildren.get(0)).isInstanceOf(GoSyntheticLibraryElementNode.class); + assertThat( + ((BlazeGoExternalSyntheticLibrary) actualChildren.get(0).getValue()).getSourceRoots()) + .containsExactly(file); + } + + @Test + public void givenWrongRootNode_doNothing() { + String fileName = "bar.go"; + VirtualFile file = MockVirtualFile.file(fileName); + PsiFileNode fileNode = createPsiFileNode(file); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file), "root"); + + Collection> actualChildren = + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ createRootNode("Wrong root node"), + /* children= */ ImmutableList.of(fileNode), + /* settings= */ new ViewSettings() {}); + + assertThat(actualChildren).containsExactly(fileNode); + } + + @Test + public void givenNotInBlazeProject_doNothing() { + BlazeImportSettingsManager.getInstance(intellij.getProject()).setImportSettings(null); + String fileName = "bar.go"; + VirtualFile file = MockVirtualFile.file(fileName); + PsiFileNode fileNode = createPsiFileNode(file); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file), "root"); + + Collection> actualChildren = + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(fileNode), + /* settings= */ new ViewSettings() {}); + + assertThat(actualChildren).containsExactly(fileNode); + } + + @Test + public void givenNodeNotInOriginalList_doNothing() { + String fileName = "bar.go"; + VirtualFile file = MockVirtualFile.file(fileName); + fileToImportPathMap.put(VfsUtil.virtualToIoFile(file), "root"); + + Collection> actualChildren = + new BlazeGoTreeStructureProvider() + .modify( + /* parent= */ rootNode, + /* children= */ ImmutableList.of(), + /* settings= */ new ViewSettings() {}); + + assertThat(actualChildren).isEmpty(); + } + + private PsiFileNode createPsiFileNode(VirtualFile virtualFile) { + return new PsiFileNode( + intellij.getProject(), + new FakePsiFile(intellij.getProject(), virtualFile), + new ViewSettings() {}); + } + + private SyntheticLibraryElementNode createRootNode(String nodeName) { + BlazeExternalSyntheticLibrary parentLibrary = + new BlazeExternalSyntheticLibrary( + /* presentableText= */ nodeName, /* files= */ ImmutableList.of()); + + return new SyntheticLibraryElementNode( + intellij.getProject(), parentLibrary, parentLibrary, new ViewSettings() {}); + } + + private static final class FakeTreeAnchorizer extends TreeAnchorizer { + @Override + public Object createAnchor(Object element) { + return element; + } + + private FakeTreeAnchorizer() {} + } + + private abstract static class FakePsiFileSystemItem extends FakePsiElement + implements PsiFileSystemItem { + final Project project; + final VirtualFile vf; + + private FakePsiFileSystemItem(Project project, VirtualFile vf) { + this.project = project; + this.vf = vf; + } + + @Override + public VirtualFile getVirtualFile() { + return vf; + } + + @Override + public Project getProject() { + return project; + } + + @Override + public String toString() { + return vf.getPath(); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean canNavigate() { + return false; + } + + @Override + public boolean canNavigateToSource() { + return false; + } + + @Override + public void navigate(boolean requestFocus) {} + + @Override + public boolean processChildren(PsiElementProcessor processor) { + return false; + } + + @Override + public void checkSetName(String name) {} + } + + private static final class FakePsiFile extends FakePsiFileSystemItem implements PsiFile { + + private FakePsiFile(Project project, VirtualFile vf) { + super(project, vf); + } + + @Nullable + @Override + public FileASTNode getNode() { + return null; + } + + @Override + @Nullable + public PsiDirectory getParent() { + return null; + } + + @Override + public PsiFile getContainingFile() { + return this; + } + + @Override + public PsiDirectory getContainingDirectory() { + return null; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public long getModificationStamp() { + return 0; + } + + @Override + public PsiFile getOriginalFile() { + return this; + } + + @Override + public FileType getFileType() { + return FileTypeManager.getInstance().getFileTypeByFileName(vf.getName()); + } + + @Override + public PsiFile[] getPsiRoots() { + return PsiFile.EMPTY_ARRAY; + } + + @Override + public FileViewProvider getViewProvider() { + return Objects.requireNonNull(PsiManager.getInstance(project).findViewProvider(vf)); + } + + @Override + public void subtreeChanged() {} + } +}