diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml index 4c93d7ed3ac..6e8c748b56f 100644 --- a/base/src/META-INF/blaze-base.xml +++ b/base/src/META-INF/blaze-base.xml @@ -376,6 +376,7 @@ + diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelCharFilter.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelCharFilter.java new file mode 100644 index 00000000000..061ac192cb1 --- /dev/null +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelCharFilter.java @@ -0,0 +1,55 @@ +/* + * 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 com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage; +import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral; +import com.intellij.codeInsight.lookup.CharFilter; +import com.intellij.codeInsight.lookup.Lookup; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +/** Allows '@' to be typed (and appended to completion) inside a label from a build file. */ +public final class BuildLabelCharFilter extends CharFilter { + @Nullable + public CharFilter.Result acceptChar(char c, int prefixLength, @NotNull Lookup lookup) { + if (!lookup.isCompletion()) { + return null; + } + + PsiFile file = lookup.getPsiFile(); + if (file == null || + file.getLanguage() != BuildFileLanguage.INSTANCE || + PsiTreeUtil.getParentOfType(lookup.getPsiElement(), StringLiteral.class) == null) { + return null; + } + + switch (c) { + case '@': + return Result.ADD_TO_PREFIX; + + case '/': + if (lookup.getCurrentItem() instanceof ExternalWorkspaceLookupElement) { + return Result.SELECT_ITEM_AND_FINISH_LOOKUP; + } + } + + return null; + } +} diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java index 9e99d40ff8c..12d884ae605 100644 --- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java @@ -43,7 +43,7 @@ public BuildLookupElement(String baseName, QuoteType quoteWrapping) { this.wrapWithQuotes = quoteWrapping != QuoteType.NoQuotes; } - private static boolean insertClosingQuotes() { + protected static boolean insertClosingQuotes() { return CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE; } diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceLookupElement.java new file mode 100644 index 00000000000..e5b9eb2bcae --- /dev/null +++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceLookupElement.java @@ -0,0 +1,100 @@ +package com.google.idea.blaze.base.lang.buildfile.completion; + +import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral; +import com.google.idea.blaze.base.lang.buildfile.references.QuoteType; +import com.google.idea.blaze.base.model.primitives.ExternalWorkspace; +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.AutoCompletionPolicy; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.PlatformIcons; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public class ExternalWorkspaceLookupElement extends BuildLookupElement { + private final ExternalWorkspace workspace; + + public ExternalWorkspaceLookupElement(ExternalWorkspace workspace) { + super('@' + workspace.repoName(), QuoteType.NoQuotes); + this.workspace = workspace; + } + + @Override + public String getLookupString() { + return super.getItemText(); + } + + @Override + @Nullable + protected String getTypeText() { + return !workspace.repoName().equals(workspace.name()) ? '@' + workspace.name() : null; + } + + @Override + @Nullable + protected String getTailText() { + return "//"; + } + + @Override + public @Nullable Icon getIcon() { + return PlatformIcons.LIBRARY_ICON; + } + + @Override + public void handleInsert(InsertionContext context) { + StringLiteral literal = PsiTreeUtil.findElementOfClassAtOffset(context.getFile(), context.getStartOffset(), StringLiteral.class, false); + if (literal == null) { + super.handleInsert(context); + return; + } + + Document document = context.getDocument(); + context.commitDocument(); + + // if we completed by pressing '/' (since this lookup element should never complete using '/') . + if (context.getCompletionChar() == '/') { + context.setAddCompletionChar(false); + } + + CaretModel caret = context.getEditor().getCaretModel(); + // find an remove trailing package path after insert / replace. + // current element text looks like `@workspace`. If this is complete inside an existing workspace name the + // result would look like: @workspaceold_workspace_path//. The following bit will remove `old_workspace_path//` + int replaceStart = context.getTailOffset(); + int replaceEnd = context.getTailOffset(); + + int indexOfPackageStart = literal.getText().lastIndexOf("//"); + if (indexOfPackageStart != -1) { + // shift to be a document offset + replaceEnd = indexOfPackageStart + 2 + literal.getTextOffset(); + } + + document.replaceString(replaceStart, replaceEnd, "//"); + context.commitDocument(); + caret.moveToOffset(replaceStart + 2); + + // handle auto insertion of end quote. This will have to be if the completion is triggered inside a `"@ workspaceRootCache = SyncCache.getInstance(project) 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 new file mode 100644 index 00000000000..2b0e0811c79 --- /dev/null +++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ExternalWorkspaceCompletionTest.java @@ -0,0 +1,107 @@ +package com.google.idea.blaze.base.lang.buildfile.completion; + +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.codeInsight.lookup.Lookup; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupEx; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; +import org.junit.Ignore; +import org.junit.Test; + +import static com.google.common.truth.ExpectFailure.expectFailure; +import static com.google.common.truth.Truth.assertThat; + +public class ExternalWorkspaceCompletionTest extends BuildFileIntegrationTestCase { + protected ExternalWorkspaceFixture workspaceOne; + protected ExternalWorkspaceFixture workspaceTwoMapped; + + @Override + protected ExternalWorkspaceData mockExternalWorkspaceData() { + workspaceOne = new ExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_one", "workspace_one"), fileSystem); + + workspaceTwoMapped = new ExternalWorkspaceFixture( + ExternalWorkspace.create("workspace_two", "com_workspace_two"), fileSystem); + + return ExternalWorkspaceData.create( + ImmutableList.of(workspaceOne.workspace, workspaceTwoMapped.workspace)); + } + + @Test + public void testEmptyLabelCompletion() throws Throwable { + PsiFile file = testFixture.configureByText("BUILD", "''"); + + String[] strings = editorTest.getCompletionItemsAsStrings(); + assertThat(strings).hasLength(2); + assertThat(strings) + .asList() + .containsExactly("@com_workspace_two", "@workspace_one"); + } + + @Test + public void testCompleteWillIncludeSlashes () { + PsiFile file = testFixture.configureByText("BUILD", "'@com'"); + assertThat(editorTest.completeIfUnique()).isTrue(); + + assertFileContents(file, "'@com_workspace_two//'"); + } + + @Test + public void testCompleteWillFixUpRemainingSlashed () { + PsiFile file = testFixture.configureByText("BUILD", "'@com//'"); + assertThat(editorTest.completeIfUnique()).isTrue(); + + assertFileContents(file, "'@com_workspace_two//'"); + } + + @Test + public void testCompleteWillAlwaysReplaceWorkspace() { + PsiFile file = testFixture.configureByText("BUILD", "'@comxxxx//'"); + assertThat(editorTest.completeIfUnique()).isTrue(); + + assertFileContents(file, "'@com_workspace_two//'"); + } + + @Test + public void testCompleteWillRespectAutoQuoting() { + boolean old = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE; + try { + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = false; + PsiFile file = testFixture.configureByText("BUILD", "'@com"); + + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//"); + + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = true; + file = testFixture.configureByText("BUILD", "'@com"); + + assertThat(editorTest.completeIfUnique()).isTrue(); + assertFileContents(file, "'@com_workspace_two//'"); + + } finally { + CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = old; + } + } + + @Test + public void testSlashWillAutoCompleteCurrentItem() throws Throwable { + PsiFile file = testFixture.configureByText("BUILD", "'@comxxxx//'"); + + assertThat(testFixture.completeBasic()).isNotNull(); + LookupEx lookup = testFixture.getLookup(); + + LookupElement currentItem = lookup.getCurrentItem(); + assertThat(currentItem).isNotNull(); + + testFixture.type('/'); + assertFileContents(file, String.format("'%s//'", currentItem.getLookupString())); + } +} 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 a2f8ce53d7f..3728c040bdb 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 @@ -91,6 +91,6 @@ protected File getExternalSourceRoot() { BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData(); assertThat(blazeProjectData).isNotNull(); - return WorkspaceHelper.getExternalSourceRoot(blazeProjectData); + return WorkspaceHelper.getExternalSourceRoot(blazeProjectData).toFile(); } } diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java index 404c0bd5855..c913d85c9ab 100644 --- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java +++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java @@ -149,7 +149,7 @@ private static WorkspacePath getWorkspacePathForExternalTarget( Project project, WorkspacePathResolver workspacePathResolver) { if (target.toTargetInfo().getLabel().isExternal()) { - WorkspaceRoot externalWorkspace = WorkspaceHelper.resolveExternalWorkspace(project, + WorkspaceRoot externalWorkspace = WorkspaceHelper.getExternalWorkspace(project, target.getKey().getLabel().externalWorkspaceName()); if (externalWorkspace != null) { diff --git a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java index 1b69d7450f2..fc479a858c6 100644 --- a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java +++ b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java @@ -826,7 +826,7 @@ public void testExternalDependencyResolvedWhenIsPartOfProject() throws IOExcepti .build(); File externalRoot = WorkspaceHelper.getExternalSourceRoot( - BlazeProjectDataManager.getInstance(project).getBlazeProjectData()); + BlazeProjectDataManager.getInstance(project).getBlazeProjectData()).toFile(); File spyExternalDependencyRoot = spy(new File(externalRoot, "external_dependency")); doReturn(true).when(spyExternalDependencyRoot).isDirectory(); @@ -844,7 +844,7 @@ public void testExternalDependencyResolvedWhenIsPartOfProject() throws IOExcepti try (MockedStatic mockedStatic = Mockito.mockStatic(WorkspaceHelper.class)) { mockedStatic.when( - () -> WorkspaceHelper.resolveExternalWorkspace(Mockito.any(MockProject.class), + () -> WorkspaceHelper.getExternalWorkspace(Mockito.any(MockProject.class), Mockito.any(String.class))).thenReturn(new WorkspaceRoot(spyExternalDependencyRoot)); assertThatResolving(projectView, targetMap).producesConfigurationsFor(