From b3eb5be070c0413cb98441ed17065e4f6291619f Mon Sep 17 00:00:00 2001 From: Mihai Toader Date: Sat, 7 Sep 2024 17:05:40 -0700 Subject: [PATCH] [#6664] Completion of FilePath and targets from external repositories --- .../completion/LabelRuleLookupElement.java | 27 ++- .../references/BuildReferenceManager.java | 59 ++++-- .../buildfile/references/FileLookupData.java | 14 +- .../buildfile/references/LabelReference.java | 18 +- .../lang/buildfile/references/LabelUtils.java | 18 +- .../references/PackageReferenceFragment.java | 8 +- .../references/ProjectViewLabelReference.java | 6 +- .../base/sync/workspace/WorkspaceHelper.java | 1 + .../ExternalWorkspaceCompletionTest.java | 20 +- ...PathInExternalWorkspaceCompletionTest.java | 173 ++++++++++++++++++ .../completion/RuleTargetCompletionTest.java | 5 - ...rgetInExternalWorkspaceCompletionTest.java | 138 ++++++++++++++ .../ExternalWorkspaceFindUsagesTest.java | 1 + ...ExternalWorkspaceReferenceBzlModeTest.java | 36 ++-- .../blaze/base/ExternalWorkspaceFixture.java | 18 +- .../BuildFileIntegrationTestCase.java | 13 ++ 16 files changed, 473 insertions(+), 82 deletions(-) create mode 100644 base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathInExternalWorkspaceCompletionTest.java create mode 100644 base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetInExternalWorkspaceCompletionTest.java diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java index 8442d362085..6c1ec6da539 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -32,16 +32,10 @@ public class LabelRuleLookupElement extends BuildLookupElement { public static BuildLookupElement[] collectAllRules( - BuildFile file, - String originalString, - String packagePrefix, - @Nullable String excluded, - QuoteType quoteType) { - if (packagePrefix.startsWith("//") || originalString.startsWith(":")) { - packagePrefix += ":"; - } + BuildFile file, String originalLabel, @Nullable String excluded, QuoteType quoteType) { + + String ruleFragment = LabelUtils.getRuleComponent(originalLabel); - String ruleFragment = LabelUtils.getRuleComponent(originalString); List lookups = Lists.newArrayList(); for (FuncallExpression target : file.findChildrenByClass(FuncallExpression.class)) { String targetName = target.getName(); @@ -54,12 +48,15 @@ public static BuildLookupElement[] collectAllRules( if (ruleType == null) { continue; } + String lookupPrefix = + originalLabel.substring(0, originalLabel.length() - ruleFragment.length()); + lookups.add( - new LabelRuleLookupElement(packagePrefix, target, targetName, ruleType, quoteType)); + new LabelRuleLookupElement(lookupPrefix, target, targetName, ruleType, quoteType)); } return lookups.isEmpty() ? BuildLookupElement.EMPTY_ARRAY - : lookups.toArray(new BuildLookupElement[lookups.size()]); + : lookups.toArray(BuildLookupElement.EMPTY_ARRAY); } private final FuncallExpression target; @@ -67,17 +64,17 @@ public static BuildLookupElement[] collectAllRules( private final String ruleType; private LabelRuleLookupElement( - String packagePrefix, + String namePrefix, FuncallExpression target, String targetName, String ruleType, QuoteType quoteType) { - super(packagePrefix + targetName, quoteType); + super(namePrefix + targetName, quoteType); this.target = target; this.targetName = targetName; this.ruleType = ruleType; - assert (packagePrefix.isEmpty() || packagePrefix.endsWith(":")); + assert (namePrefix.isEmpty() || namePrefix.endsWith(":")); } @Override diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java index f31707afa27..7b2db82c4a1 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -25,6 +25,7 @@ import com.google.idea.blaze.base.model.primitives.Label; import com.google.idea.blaze.base.model.primitives.TargetName; import com.google.idea.blaze.base.model.primitives.WorkspacePath; +import com.google.idea.blaze.base.model.primitives.WorkspaceRoot; import com.google.idea.blaze.base.settings.Blaze; import com.google.idea.blaze.base.sync.projectview.ImportRoots; import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper; @@ -116,16 +117,37 @@ public PsiFileSystemItem resolveFile(File file) { @Nullable public File resolvePackage(@Nullable WorkspacePath packagePath) { - return resolveWorkspaceRelativePath(packagePath != null ? packagePath.relativePath() : null); + return resolvePackageInWorkspace(packagePath, null); } @Nullable - private File resolveWorkspaceRelativePath(@Nullable String relativePath) { - WorkspacePathResolver pathResolver = getWorkspacePathResolver(); - if (pathResolver == null || relativePath == null) { + public File resolvePackageInWorkspace( + @Nullable WorkspacePath packagePath, @Nullable String externalWorkspace) { + return resolveWorkspaceRelativePath( + packagePath != null ? packagePath.relativePath() : null, externalWorkspace); + } + + @Nullable + private File resolveWorkspaceRelativePath( + @Nullable String relativePath, @Nullable String workspace) { + if (relativePath == null) { return null; } - return pathResolver.resolveToFile(relativePath); + + if (workspace == null) { + WorkspacePathResolver pathResolver = getWorkspacePathResolver(); + if (pathResolver == null) { + return null; + } + return pathResolver.resolveToFile(relativePath); + } + + WorkspaceRoot workspaceRoot = WorkspaceHelper.getExternalWorkspace(project, workspace); + if (workspaceRoot != null) { + return workspaceRoot.absolutePathFor(relativePath).toFile(); + } + + return null; } @Nullable @@ -145,14 +167,14 @@ public BuildLookupElement[] resolvePackageLookupElements(FileLookupData lookupDa // ignore invalid labels containing './', '../', etc. return BuildLookupElement.EMPTY_ARRAY; } - File file = resolveWorkspaceRelativePath(relativePath); + File file = resolveWorkspaceRelativePath(relativePath, lookupData.externalWorkspace); FileOperationProvider provider = FileOperationProvider.getInstance(); String pathFragment = ""; if (file == null || (!provider.isDirectory(file) && !relativePath.endsWith("/"))) { // we might be partway through a file name. Try the parent directory relativePath = PathUtil.getParentPath(relativePath); - file = resolveWorkspaceRelativePath(relativePath); + file = resolveWorkspaceRelativePath(relativePath, lookupData.externalWorkspace); pathFragment = StringUtil.trimStart(lookupData.filePathFragment.substring(relativePath.length()), "/"); } @@ -217,14 +239,17 @@ private BuildLookupElement lookupForFile(VirtualFile file, FileLookupData lookup } @Nullable - public BuildFile resolveBlazePackage(String workspaceRelativePath) { + public BuildFile resolveBlazePackage( + String workspaceRelativePath, @Nullable String externalWorkspace) { workspaceRelativePath = StringUtil.trimStart(workspaceRelativePath, "//"); - return resolveBlazePackage(WorkspacePath.createIfValid(workspaceRelativePath)); + return resolveBlazePackage( + WorkspacePath.createIfValid(workspaceRelativePath), externalWorkspace); } @Nullable - public BuildFile resolveBlazePackage(@Nullable WorkspacePath path) { - return findBuildFile(resolvePackage(path)); + public BuildFile resolveBlazePackage( + @Nullable WorkspacePath path, @Nullable String externalWorkspace) { + return findBuildFile(resolvePackageInWorkspace(path, externalWorkspace)); } @Nullable @@ -253,12 +278,16 @@ public BuildFile findBuildFile(@Nullable File packageDirectory) { */ @Nullable public File resolveParentDirectory(@Nullable Label label) { - return label != null ? resolveParentDirectory(label.blazePackage(), label.targetName()) : null; + return label != null + ? resolveParentDirectory( + label.externalWorkspaceName(), label.blazePackage(), label.targetName()) + : null; } @Nullable - private File resolveParentDirectory(WorkspacePath packagePath, TargetName targetName) { - File packageFile = resolvePackage(packagePath); + private File resolveParentDirectory( + String workspaceName, WorkspacePath packagePath, TargetName targetName) { + File packageFile = resolvePackageInWorkspace(packagePath, workspaceName); if (packageFile == null) { return null; } diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java index 328ce19e210..bfbc090900f 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -39,7 +39,8 @@ /** The data relevant to finding file lookups. */ public class FileLookupData { - private static final ImmutableSet BUILDFILE_NAMES = ImmutableSet.of("BUILD", "BUILD.bazel"); + private static final ImmutableSet BUILDFILE_NAMES = + ImmutableSet.of("BUILD", "BUILD.bazel"); /** The type of path string format */ public enum PathFormat { @@ -70,7 +71,9 @@ public static FileLookupData nonLocalFileLookup( return null; } // handle the single '/' case by calling twice. - String relativePath = StringUtil.trimStart(StringUtil.trimStart(originalLabel, "/"), "/"); + String relativePath = + StringUtil.trimStart( + StringUtil.trimStart(LabelUtils.getPackagePathComponent(originalLabel), "/"), "/"); if (relativePath.startsWith("/")) { return null; } @@ -119,6 +122,7 @@ public static FileLookupData packageLocalFileLookup( private final BuildFile containingFile; @Nullable private final String containingPackage; public final String filePathFragment; + @Nullable public final String externalWorkspace; public final PathFormat pathFormat; private final QuoteType quoteType; @Nullable private final VirtualFileFilter fileFilter; @@ -139,6 +143,7 @@ protected FileLookupData( this.filePathFragment = filePathFragment; this.pathFormat = pathFormat; this.quoteType = quoteType; + this.externalWorkspace = LabelUtils.getExternalWorkspaceComponent(originalLabel); assert (pathFormat != PathFormat.PackageLocal || (containingPackage != null && containingFile != null)); @@ -187,6 +192,9 @@ private String getFullLabel(String relativePath) { if (pathFormat != PathFormat.PackageLocal) { if (pathFormat == PathFormat.NonLocal) { relativePath = "//" + relativePath; + if (externalWorkspace != null) { + relativePath = "@" + externalWorkspace + relativePath; + } } return relativePath; } diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java index f2c28dd4caa..8f62aac91c2 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -98,15 +98,21 @@ public Object[] getVariants() { private BuildLookupElement[] getRuleLookups(String labelString) { if (labelString.endsWith("/") || (labelString.startsWith("/") && !labelString.contains(":")) + || (labelString.startsWith("@") && !labelString.contains(":")) || skylarkExtensionReference(myElement)) { return BuildLookupElement.EMPTY_ARRAY; } + String packagePrefix = LabelUtils.getPackagePathComponent(labelString); + String externalWorkspace = LabelUtils.getExternalWorkspaceComponent(labelString); + BuildFile referencedBuildFile = - LabelUtils.getReferencedBuildFile(myElement.getContainingFile(), packagePrefix); + LabelUtils.getReferencedBuildFile( + myElement.getContainingFile(), externalWorkspace, packagePrefix); if (referencedBuildFile == null) { return BuildLookupElement.EMPTY_ARRAY; } + String self = null; if (referencedBuildFile == myElement.getContainingFile()) { FuncallExpression funcall = @@ -116,11 +122,11 @@ private BuildLookupElement[] getRuleLookups(String labelString) { } } return LabelRuleLookupElement.collectAllRules( - referencedBuildFile, labelString, packagePrefix, self, myElement.getQuoteType()); + referencedBuildFile, labelString, self, myElement.getQuoteType()); } private BuildLookupElement[] getFileLookups(String labelString) { - if (labelString.startsWith("//") || labelString.equals("/")) { + if (labelString.startsWith("//") || labelString.equals("/") || labelString.startsWith("@")) { return getNonLocalFileLookups(labelString); } return getPackageLocalFileLookups(labelString); @@ -151,6 +157,7 @@ private BuildLookupElement[] getSkylarkExtensionLookups(String labelString) { return BuildLookupElement.EMPTY_ARRAY; } String packagePrefix = LabelUtils.getPackagePathComponent(labelString); + String externalWorkspace = LabelUtils.getExternalWorkspaceComponent(labelString); BuildFile parentFile = myElement.getContainingFile(); if (parentFile == null) { return BuildLookupElement.EMPTY_ARRAY; @@ -160,7 +167,8 @@ private BuildLookupElement[] getSkylarkExtensionLookups(String labelString) { return BuildLookupElement.EMPTY_ARRAY; } BuildFile referencedBuildFile = - LabelUtils.getReferencedBuildFile(containingPackage.buildFile, packagePrefix); + LabelUtils.getReferencedBuildFile( + containingPackage.buildFile, externalWorkspace, packagePrefix); // Directories before the colon are already covered. // We're only concerned with package-local directories. diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java index 25f1613c931..9a7fd5d4ec2 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -24,9 +24,11 @@ import com.google.idea.blaze.base.model.primitives.TargetName; import com.google.idea.blaze.base.model.primitives.WorkspacePath; import com.intellij.codeInsight.completion.CompletionUtilCore; +import com.intellij.util.ObjectUtils; import com.intellij.util.PathUtil; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Utility methods for working with blaze labels. */ @@ -119,7 +121,9 @@ public static Label createLabelFromString( /** The blaze file referenced by the label. */ @Nullable public static BuildFile getReferencedBuildFile( - @Nullable BuildFile containingFile, String packagePathComponent) { + @Nullable BuildFile containingFile, + @Nullable String externalWorkspaceComponent, + String packagePathComponent) { if (containingFile == null) { return null; } @@ -127,10 +131,18 @@ public static BuildFile getReferencedBuildFile( return containingFile; } return BuildReferenceManager.getInstance(containingFile.getProject()) - .resolveBlazePackage(packagePathComponent); + .resolveBlazePackage(packagePathComponent, externalWorkspaceComponent); } + @Nonnull public static String getRuleComponent(String labelString) { + if (labelString.startsWith("@")) { + int colonIndex = labelString.indexOf('/'); + if (colonIndex == -1) { + return ""; + } + labelString = labelString.substring(colonIndex + 1); + } if (labelString.startsWith("/")) { int colonIndex = labelString.indexOf(':'); return colonIndex == -1 ? "" : labelString.substring(colonIndex + 1); diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java index 746d4e5b41e..97d20a3fbc2 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -62,9 +62,11 @@ public TextRange getRangeInElement() { @Nullable @Override public BuildFile resolve() { - WorkspacePath workspacePath = getWorkspacePath(myElement.getStringContents()); + String labelString = myElement.getStringContents(); + WorkspacePath workspacePath = getWorkspacePath(labelString); + String externalWorkspace = LabelUtils.getExternalWorkspaceComponent(labelString); return BuildReferenceManager.getInstance(myElement.getProject()) - .resolveBlazePackage(workspacePath); + .resolveBlazePackage(workspacePath, externalWorkspace); } @Override diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java b/base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java index 6dea9671a98..86e71faaead 100644 --- a/base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java +++ b/base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The Bazel Authors. All rights reserved. + * Copyright 2024 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. @@ -109,12 +109,12 @@ private BuildLookupElement[] getRuleLookups(String labelString) { } String packagePrefix = LabelUtils.getPackagePathComponent(labelString); BuildFile referencedBuildFile = - BuildReferenceManager.getInstance(getProject()).resolveBlazePackage(packagePrefix); + BuildReferenceManager.getInstance(getProject()).resolveBlazePackage(packagePrefix, null); if (referencedBuildFile == null) { return BuildLookupElement.EMPTY_ARRAY; } return LabelRuleLookupElement.collectAllRules( - referencedBuildFile, labelString, packagePrefix, null, QuoteType.NoQuotes); + referencedBuildFile, labelString, null, QuoteType.NoQuotes); } private BuildLookupElement[] getFileLookups(String labelString) { diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java index c4595ebaaf8..ac975e44e92 100644 --- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java +++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java @@ -190,6 +190,7 @@ public static Path getExternalSourceRoot(BlazeProjectData projectData) { return Paths.get(projectData.getBlazeInfo().getOutputBase().getAbsolutePath(), "external").normalize(); } + /** resolve workspace root for a named external repository. needs context since the same name can mean something different in different workspaces. */ @Nullable private static synchronized WorkspaceRoot resolveExternalWorkspaceRoot( Project project, String workspaceName, @Nullable BuildFile buildFile) { diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceCompletionTest.java index 2b0e0811c79..096cf43a148 100644 --- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceCompletionTest.java +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceCompletionTest.java @@ -25,11 +25,12 @@ public class ExternalWorkspaceCompletionTest extends BuildFileIntegrationTestCas @Override protected ExternalWorkspaceData mockExternalWorkspaceData() { - workspaceOne = new ExternalWorkspaceFixture( - ExternalWorkspace.create("workspace_one", "workspace_one"), fileSystem); + workspaceOne = + createExternalWorkspaceFixture(ExternalWorkspace.create("workspace_one", "workspace_one")); - workspaceTwoMapped = new ExternalWorkspaceFixture( - ExternalWorkspace.create("workspace_two", "com_workspace_two"), fileSystem); + workspaceTwoMapped = + createExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_two", "com_workspace_two")); return ExternalWorkspaceData.create( ImmutableList.of(workspaceOne.workspace, workspaceTwoMapped.workspace)); @@ -41,13 +42,11 @@ public void testEmptyLabelCompletion() throws Throwable { String[] strings = editorTest.getCompletionItemsAsStrings(); assertThat(strings).hasLength(2); - assertThat(strings) - .asList() - .containsExactly("@com_workspace_two", "@workspace_one"); + assertThat(strings).asList().containsExactly("@com_workspace_two", "@workspace_one"); } @Test - public void testCompleteWillIncludeSlashes () { + public void testCompleteWillIncludeSlashes() { PsiFile file = testFixture.configureByText("BUILD", "'@com'"); assertThat(editorTest.completeIfUnique()).isTrue(); @@ -55,7 +54,7 @@ public void testCompleteWillIncludeSlashes () { } @Test - public void testCompleteWillFixUpRemainingSlashed () { + public void testCompleteWillFixUpRemainingSlashed() { PsiFile file = testFixture.configureByText("BUILD", "'@com//'"); assertThat(editorTest.completeIfUnique()).isTrue(); @@ -73,9 +72,10 @@ public void testCompleteWillAlwaysReplaceWorkspace() { @Test public void testCompleteWillRespectAutoQuoting() { boolean old = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE; + PsiFile file; try { CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = false; - PsiFile file = testFixture.configureByText("BUILD", "'@com"); + file = testFixture.configureByText("BUILD", "'@com"); assertThat(editorTest.completeIfUnique()).isTrue(); assertFileContents(file, "'@com_workspace_two//"); diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathInExternalWorkspaceCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathInExternalWorkspaceCompletionTest.java new file mode 100644 index 00000000000..a7061246e44 --- /dev/null +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathInExternalWorkspaceCompletionTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024 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.base.lang.buildfile.completion; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.idea.blaze.base.ExternalWorkspaceFixture; +import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase; +import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile; +import com.google.idea.blaze.base.model.ExternalWorkspaceData; +import com.google.idea.blaze.base.model.primitives.ExternalWorkspace; +import com.google.idea.blaze.base.model.primitives.WorkspacePath; +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.openapi.editor.Editor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests file path code completion in BUILD file labels to paths inside visible external workspaces. + */ +@RunWith(JUnit4.class) +public class FilePathInExternalWorkspaceCompletionTest extends BuildFileIntegrationTestCase { + protected ExternalWorkspaceFixture workspace; + + @Override + protected ExternalWorkspaceData mockExternalWorkspaceData() { + workspace = + createExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_two", "com_workspace_two")); + + return ExternalWorkspaceData.create(ImmutableList.of(workspace.workspace)); + } + + @Test + public void testUniqueDirectoryCompleted() throws Throwable { + workspace.createBuildFile(new WorkspacePath("java/BUILD"), "'//'"); + + BuildFile file = + createBuildFileWithCaret(new WorkspacePath("file/BUILD"), "'@com_workspace_two//"); + + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//java'"); + // check caret remains inside closing quote + editorTest.assertCaretPosition( + testFixture.getEditor(), 0, "'@com_workspace_two//java".length()); + } + + @Test + public void testInsertPairQuoteOptionRespected() throws Throwable { + workspace.createBuildFile(new WorkspacePath("java/BUILD"), "'//'"); + + boolean old = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE; + BuildFile file; + try { + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = false; + file = + createBuildFileWithCaret( + new WorkspacePath("file1/BUILD"), "'@com_workspace_two//j"); + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//java"); + + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = true; + file = + createBuildFileWithCaret( + new WorkspacePath("file2/BUILD"), "'@com_workspace_two//j"); + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//java'"); + } finally { + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = old; + } + } + + @Test + public void testUniqueMultiSegmentDirectoryCompleted() throws Throwable { + workspace.createBuildFile(new WorkspacePath("java/com/google/BUILD"), "'//'"); + + BuildFile file = + createBuildFileWithCaret(new WorkspacePath("BUILD"), "'@com_workspace_two//"); + + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//java/com/google'"); + } + + @Test + public void testStopDirectoryTraversalAtBuildPackage() throws Throwable { + workspace.createBuildFile(new WorkspacePath("foo/bar/BUILD")); + workspace.createBuildFile(new WorkspacePath("foo/bar/baz/BUILD")); + + BuildFile file = + createBuildFileWithCaret(new WorkspacePath("file/BUILD"), "'@com_workspace_two//f"); + assertThat(editorTest.completeIfUnique()).isTrue(); + + assertFileContents(file, "'@com_workspace_two//foo/bar'"); + } + + // expected to be a typical workflow -- complete a segment, + // get the possibilities, then start typing + // next segment and complete again + @Test + public void testMultiStageCompletion() throws Throwable { + workspace.createDirectory(new WorkspacePath("foo")); + workspace.createDirectory(new WorkspacePath("bar")); + workspace.createDirectory(new WorkspacePath("other")); + workspace.createDirectory(new WorkspacePath("other/foo")); + workspace.createDirectory(new WorkspacePath("other/bar")); + + BuildFile file = + createBuildFileWithCaret(new WorkspacePath("file/BUILD"), "'@com_workspace_two//'"); + + String[] completionItems = editorTest.getCompletionItemsAsStrings(); + assertThat(completionItems).hasLength(3); + + Editor editor = testFixture.getEditor(); + editorTest.performTypingAction(editor, 'o'); + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//other'"); + editorTest.assertCaretPosition(editor, 0, "'@com_workspace_two//other".length()); + + editorTest.performTypingAction(editor, '/'); + editorTest.performTypingAction(editor, 'f'); + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//other/foo'"); + editorTest.assertCaretPosition(editor, 0, "'@com_workspace_two//other/foo".length()); + } + + @Test + public void testCompletionSuggestionString() { + workspace.createDirectory(new WorkspacePath("foo")); + workspace.createDirectory(new WorkspacePath("bar")); + workspace.createDirectory(new WorkspacePath("other")); + workspace.createDirectory(new WorkspacePath("ostrich/foo")); + workspace.createDirectory(new WorkspacePath("ostrich/fooz")); + + BuildFile file = + createBuildFileWithCaret(new WorkspacePath("BUILD"), "'@com_workspace_two//o'"); + + String[] completionItems = editorTest.getCompletionItemsAsSuggestionStrings(); + assertThat(completionItems).asList().containsExactly("other", "ostrich"); + + editorTest.performTypingAction(testFixture.getEditor(), 's'); + + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//ostrich'"); + + completionItems = editorTest.getCompletionItemsAsSuggestionStrings(); + assertThat(completionItems).asList().containsExactly("/foo", "/fooz"); + + editorTest.performTypingAction(testFixture.getEditor(), '/'); + + completionItems = editorTest.getCompletionItemsAsSuggestionStrings(); + assertThat(completionItems).asList().containsExactly("foo", "fooz"); + + editorTest.performTypingAction(testFixture.getEditor(), 'f'); + + completionItems = editorTest.getCompletionItemsAsSuggestionStrings(); + assertThat(completionItems).asList().containsExactly("foo", "fooz"); + } +} diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java index 20328d2a493..d41db2e7ecd 100644 --- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java @@ -189,11 +189,6 @@ public void testLocalPathIgnoredForNonLocalLabels() throws Throwable { assertThat(completionItems).asList().doesNotContain("'//java/com/google:other_rule'"); } - @Test - public void testExternalRepoCompletion() throws Throwable { - - } - private static void setBuildLanguageSpecRules( MockBuildLanguageSpecProvider specProvider, String... ruleNames) { ImmutableMap.Builder rules = ImmutableMap.builder(); diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetInExternalWorkspaceCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetInExternalWorkspaceCompletionTest.java new file mode 100644 index 00000000000..d702c14936e --- /dev/null +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetInExternalWorkspaceCompletionTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2024 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.base.lang.buildfile.completion; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.idea.blaze.base.ExternalWorkspaceFixture; +import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase; +import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec; +import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider; +import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition; +import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile; +import com.google.idea.blaze.base.model.ExternalWorkspaceData; +import com.google.idea.blaze.base.model.primitives.ExternalWorkspace; +import com.google.idea.blaze.base.model.primitives.WorkspacePath; +import javax.annotation.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests code completion of rule target labels inside visible external workspaces. */ +@RunWith(JUnit4.class) +public class RuleTargetInExternalWorkspaceCompletionTest extends BuildFileIntegrationTestCase { + protected ExternalWorkspaceFixture workspace; + + @Override + protected ExternalWorkspaceData mockExternalWorkspaceData() { + workspace = createExternalWorkspaceFixture(ExternalWorkspace.create("workspace+", "workspace")); + + return ExternalWorkspaceData.create(ImmutableList.of(workspace.workspace)); + } + + @Test + public void testCustomRuleCompletion() { + MockBuildLanguageSpecProvider specProvider = new MockBuildLanguageSpecProvider(); + setBuildLanguageSpecRules(specProvider, "java_library"); + registerProjectService(BuildLanguageSpecProvider.class, specProvider); + + workspace.createBuildFile(new WorkspacePath("java/BUILD"), "custom_rule(name = 'lib')"); + + createBuildFileWithCaret( + new WorkspacePath("java/com/google/BUILD"), + "java_library(", + " name = 'test',", + " deps = ['@workspace//java:']"); + + assertThat(editorTest.getCompletionItemsAsStrings()) + .asList() + .containsExactly("'@workspace//java:lib'"); + } + + @Test + public void testWorkspaceTarget() { + workspace.createBuildFile( + new WorkspacePath("java/com/google/foo/BUILD"), "java_library(name = 'foo_lib')"); + + createBuildFileWithCaret( + new WorkspacePath("java/com/google/bar/BUILD"), + "java_library(", + " name = 'bar_lib',", + " deps = '@workspace//java/com/google/foo:')"); + + assertThat(editorTest.getCompletionItemsAsStrings()) + .asList() + .containsExactly("'@workspace//java/com/google/foo:foo_lib'"); + } + + @Test + public void testNotCompletedWithoutColon() { + workspace.createBuildFile( + new WorkspacePath("java/com/google/foo/BUILD"), "java_library(name = 'foo_lib')"); + + BuildFile bar = + createBuildFileWithCaret( + new WorkspacePath("java/com/google/bar/BUILD"), + "java_library(", + " name = 'bar_lib',", + " deps = '@workspace//java/com/google/foo')"); + + String[] completionItems = editorTest.getCompletionItemsAsStrings(); + assertThat(completionItems).isEmpty(); + } + + @Test + public void testLocalPathIgnored() { + workspace.createBuildFile(new WorkspacePath("java/BUILD"), "java_library(name = 'root_rule')"); + + createBuildFileWithCaret( + new WorkspacePath("java/com/google/BUILD"), + "java_library(name = 'other_rule')", + "java_library(", + " name = 'lib',", + " deps = ['@workspace//java:']"); + + String[] completionItems = editorTest.getCompletionItemsAsStrings(); + assertThat(completionItems).asList().contains("'@workspace//java:root_rule'"); + assertThat(completionItems).asList().doesNotContain("'//java/com/google:other_rule'"); + } + + private static void setBuildLanguageSpecRules( + MockBuildLanguageSpecProvider specProvider, String... ruleNames) { + ImmutableMap.Builder rules = ImmutableMap.builder(); + for (String name : ruleNames) { + rules.put(name, new RuleDefinition(name, ImmutableMap.of(), null)); + } + specProvider.setRules(rules.build()); + } + + private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider { + + BuildLanguageSpec languageSpec = new BuildLanguageSpec(ImmutableMap.of()); + + void setRules(ImmutableMap rules) { + languageSpec = new BuildLanguageSpec(rules); + } + + @Nullable + @Override + public BuildLanguageSpec getLanguageSpec() { + return languageSpec; + } + } +} diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java index 74d41032f4d..1d74c8b16c2 100644 --- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.idea.blaze.base.lang.buildfile.findusages; import static com.google.common.truth.Truth.assertThat; diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceBzlModeTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceBzlModeTest.java index e35a8ef8ba5..20658f85474 100644 --- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceBzlModeTest.java +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceBzlModeTest.java @@ -18,24 +18,23 @@ import org.junit.runners.JUnit4; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNotNull; @RunWith(JUnit4.class) public class ExternalWorkspaceReferenceBzlModeTest extends BuildFileIntegrationTestCase { protected ExternalWorkspaceFixture workspaceOne; - protected ExternalWorkspaceFixture workspaceTwoMapped; + protected ExternalWorkspaceFixture workspaceTwo; @Override protected ExternalWorkspaceData mockExternalWorkspaceData() { - workspaceOne = new ExternalWorkspaceFixture( - ExternalWorkspace.create("workspace_one", "workspace_one"), fileSystem); + workspaceOne = createExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_one", "workspace_one")); - workspaceTwoMapped = new ExternalWorkspaceFixture( - ExternalWorkspace.create("workspace_two", "com_workspace_two"), fileSystem); + workspaceTwo = createExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_two", "com_workspace_two")); return ExternalWorkspaceData.create( - ImmutableList.of(workspaceOne.workspace, workspaceTwoMapped.workspace)); + ImmutableList.of(workspaceOne.workspace, workspaceTwo.workspace)); } @Before @@ -44,7 +43,7 @@ public void doSetupExternalWorkspaces() { new WorkspacePath("p1/p2/BUILD"), "java_library(name = 'rule1')"); - workspaceTwoMapped.createBuildFile( + workspaceTwo.createBuildFile( new WorkspacePath("p1/p2/BUILD"), "java_library(name = 'rule1')"); } @@ -55,33 +54,32 @@ public void testUnmappedExternalWorkspace() throws Throwable { new WorkspacePath("p1/p2"), TargetName.create("rule1")); PsiFile file = testFixture.configureByText("BUILD", - String.format(""" + """ java_library( name = 'lib', - deps = ['%s'] - )""", targetLabel)); - Editor editor = editorTest.openFileInEditor(file.getVirtualFile()); + deps = ['@workspace_one//p1/p2:rule1'] + )"""); + Editor editor = testFixture.getEditor(); PsiElement target = GotoDeclarationAction.findTargetElement( getProject(), editor, editor.getCaretModel().getOffset()); - assertThat(target).isNotNull(); } @Test public void testRemappedExternalWorkspace() throws Throwable { - Label targetLabel = workspaceTwoMapped.createLabel( + Label targetLabel = workspaceTwo.createLabel( new WorkspacePath("p1/p2"), TargetName.create("rule1")); - PsiFile file = testFixture.configureByText("BUILD", - String.format(""" + testFixture.configureByText("BUILD", + """ java_library( name = 'lib', - deps = ['%s'] - )""", targetLabel)); - Editor editor = editorTest.openFileInEditor(file.getVirtualFile()); + deps = ['@com_workspace_two//p1/p2:rule1'] + )"""); + Editor editor = testFixture.getEditor(); PsiElement target = GotoDeclarationAction.findTargetElement( getProject(), editor, editor.getCaretModel().getOffset()); diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/ExternalWorkspaceFixture.java b/base/tests/utils/integration/com/google/idea/blaze/base/ExternalWorkspaceFixture.java index 99191f8a9ab..28ce9082a7c 100644 --- a/base/tests/utils/integration/com/google/idea/blaze/base/ExternalWorkspaceFixture.java +++ b/base/tests/utils/integration/com/google/idea/blaze/base/ExternalWorkspaceFixture.java @@ -8,7 +8,9 @@ import com.google.idea.blaze.base.model.primitives.WorkspacePath; import com.google.idea.blaze.base.model.primitives.WorkspaceRoot; import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; +import com.intellij.testFramework.fixtures.CodeInsightTestFixture; import java.io.File; import java.nio.file.Paths; @@ -21,11 +23,17 @@ public class ExternalWorkspaceFixture { public final ExternalWorkspace workspace; final TestFileSystem fileSystem; + final CodeInsightTestFixture testFixture; WorkspaceFileSystem workspaceFileSystem; - public ExternalWorkspaceFixture(ExternalWorkspace workspace, TestFileSystem fileSystem) { + public ExternalWorkspaceFixture(ExternalWorkspace workspace, TestFileSystem fileSystem, CodeInsightTestFixture testFixture) { this.workspace = workspace; this.fileSystem = fileSystem; + this.testFixture = testFixture; + } + + public VirtualFile createDirectory(WorkspacePath path) { + return getWorkspaceFileSystem().createDirectory(path); } public BuildFile createBuildFile(WorkspacePath workspacePath, String... contentLines) { @@ -34,6 +42,14 @@ public BuildFile createBuildFile(WorkspacePath workspacePath, String... contentL return (BuildFile) file; } + protected BuildFile configureByFile(WorkspacePath workspacePath, String... contentLines) { + PsiFile file = getWorkspaceFileSystem().createPsiFile(workspacePath, contentLines); + assertThat(file).isInstanceOf(BuildFile.class); + testFixture.configureFromExistingVirtualFile(file.getVirtualFile()); + return (BuildFile) file; + } + + WorkspaceFileSystem getWorkspaceFileSystem() { if (workspaceFileSystem == null) { BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData(); diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java index 3728c040bdb..2d57a8355a5 100644 --- a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java +++ b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java @@ -18,11 +18,13 @@ import com.google.common.base.Joiner; import com.google.idea.blaze.base.BlazeIntegrationTestCase; import com.google.idea.blaze.base.EditorTestHelper; +import com.google.idea.blaze.base.ExternalWorkspaceFixture; import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile; import com.google.idea.blaze.base.model.BlazeProjectData; import com.google.idea.blaze.base.model.ExternalWorkspaceData; import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder; import com.google.idea.blaze.base.model.MockBlazeProjectDataManager; +import com.google.idea.blaze.base.model.primitives.ExternalWorkspace; import com.google.idea.blaze.base.model.primitives.WorkspacePath; import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager; import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper; @@ -66,6 +68,13 @@ protected BuildFile createBuildFile(WorkspacePath workspacePath, String... conte return (BuildFile) file; } + protected BuildFile createBuildFileWithCaret(WorkspacePath workspacePath, String... contentLines) { + PsiFile file = workspace.createPsiFile(workspacePath, contentLines); + assertThat(file).isInstanceOf(BuildFile.class); + testFixture.configureFromExistingVirtualFile(file.getVirtualFile()); + return (BuildFile) file; + } + protected void assertFileContents(VirtualFile file, String... contentLines) { assertFileContents(fileSystem.getPsiFile(file), contentLines); } @@ -93,4 +102,8 @@ protected File getExternalSourceRoot() { return WorkspaceHelper.getExternalSourceRoot(blazeProjectData).toFile(); } + + protected ExternalWorkspaceFixture createExternalWorkspaceFixture(ExternalWorkspace workspace) { + return new ExternalWorkspaceFixture(workspace, fileSystem, testFixture); + } }