diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml index f7e616459be..4926a73c467 100644 --- a/base/src/META-INF/blaze-base.xml +++ b/base/src/META-INF/blaze-base.xml @@ -375,6 +375,10 @@ + + diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectView.java b/base/src/com/google/idea/blaze/base/projectview/ProjectView.java index c773263fd61..b13ea80f924 100644 --- a/base/src/com/google/idea/blaze/base/projectview/ProjectView.java +++ b/base/src/com/google/idea/blaze/base/projectview/ProjectView.java @@ -117,6 +117,14 @@ public static class Builder { sections.addAll(projectView.sections); } + @SuppressWarnings("unchecked") + public > List getAll(SectionKey key) { + return sections.stream() + .filter(section -> section.isSectionType(key)) + .map(section -> (SectionType) section) + .toList(); + } + /** Gets the last section of the type in the builder. Useful to add on to sections. */ @SuppressWarnings("unchecked") @Nullable diff --git a/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java b/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java index 69259d8614c..67fef35b23b 100644 --- a/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java +++ b/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java @@ -128,6 +128,7 @@ public ProjectViewSet getResult() { public static String projectViewToString(ProjectView projectView) { StringBuilder sb = new StringBuilder(); + int curLineIndex = 0; List sectionParsers = Sections.getParsers(); for (Section section : projectView.getSections()) { SectionParser sectionParser = diff --git a/base/src/com/google/idea/blaze/base/projectview/section/DirectoryLineMarkerProvider.java b/base/src/com/google/idea/blaze/base/projectview/section/DirectoryLineMarkerProvider.java new file mode 100644 index 00000000000..f06cedff3fb --- /dev/null +++ b/base/src/com/google/idea/blaze/base/projectview/section/DirectoryLineMarkerProvider.java @@ -0,0 +1,197 @@ +/* + * 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.projectview.section; + +import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiListItem; +import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiListSection; +import com.google.idea.blaze.base.model.primitives.TargetExpression; +import com.google.idea.blaze.base.model.primitives.WorkspacePath; +import com.google.idea.blaze.base.projectview.ProjectView; +import com.google.idea.blaze.base.projectview.ProjectViewEdit; +import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry; +import com.google.idea.blaze.base.projectview.section.sections.DirectorySection; +import com.google.idea.blaze.base.projectview.section.sections.TargetSection; +import com.google.idea.blaze.base.settings.ui.AddDirectoryToProjectAction; +import com.google.idea.common.experiments.BoolExperiment; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.PsiElement; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * A line marker provider for project view files, showing a plus icon for adding new directories. + */ +public class DirectoryLineMarkerProvider implements LineMarkerProvider { + + private static final BoolExperiment enabled = new BoolExperiment("projectview.directory.section.gutter.icons.enabled", true); + + @Nullable + @Override + @SuppressWarnings("rawtypes") + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) { + if (enabled.getValue() && element instanceof LeafPsiElement leafPsiElement && leafPsiElement.getStartOffsetInParent() == 0) { + if (element.getText().equals("directories") && + leafPsiElement.getParent() instanceof ProjectViewPsiListSection) { + return new LineMarkerInfo<>( + element, + element.getTextRange(), + AllIcons.Actions.AddFile, + psi -> "Add Directory to Project", + (e, elt) -> AddDirectoryToProjectAction.runAction(element.getProject(), null), + GutterIconRenderer.Alignment.RIGHT, + () -> "Add Directory to Project"); + } else if (element.getParent() instanceof ProjectViewPsiListItem parent) { + var parentTag = parent.getParent().getFirstChild().getText(); + + if (parentTag.equals("directories") || parentTag.equals("targets")) { + var disabled = element.getText().startsWith("-"); + var txt = disabled ? "Enable" : "Disable"; + var icon = disabled ? AllIcons.Diff.GutterCheckBox : AllIcons.Diff.GutterCheckBoxSelected; + + return new LineMarkerInfo<>( + element, + element.getTextRange(), + icon, + psi -> txt, + (e, elt) -> toggleSectionItem(elt, disabled, parentTag), + GutterIconRenderer.Alignment.RIGHT, + () -> txt); + } + } + } + + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List elements, @NotNull Collection> result) { + } + + private void toggleSectionItem(PsiElement elt, boolean disabled, String parentTag) { + ProjectViewEdit.ProjectViewEditor action = switch (parentTag) { + case "directories" -> (builder) -> toggleDirectory(builder, elt, disabled); + case "targets" -> (builder) -> toggleTarget(builder, elt, disabled); + default -> null; + }; + + if (action == null) { + Messages.showErrorDialog( + "Could not modify project view: invalid tag %s.".formatted(parentTag), + "Error"); + return; + } + + var edit = ProjectViewEdit.editLocalProjectView(elt.getProject(), action); + + if (edit == null) { + Messages.showErrorDialog( + "Could not modify project view. Check for errors in your project view and try again", + "Error"); + return; + } + + edit.apply(); + } + + private static boolean toggleDirectory(ProjectView.Builder builder, PsiElement elt, boolean disabled) { + int elementLineNumber = getPsiElementLineNumber(elt); + + var optionalSection = getListByLineNumber(builder, DirectorySection.KEY, elementLineNumber); + + if (optionalSection.isEmpty()) { + return false; + } + + var listSection = optionalSection.get(); + + var directoriesUpdater = ListSection.update(DirectorySection.KEY, listSection); + + var directoryStr = elt.getText().substring(disabled ? 1 : 0); + + directoriesUpdater.replaceElement( + elementLineNumber, + disabled ? + DirectoryEntry.include(new WorkspacePath(directoryStr)) : + DirectoryEntry.exclude(new WorkspacePath(directoryStr)) + ); + + builder.replace(listSection, directoriesUpdater); + + return true; + } + + private static boolean toggleTarget(ProjectView.Builder builder, PsiElement elt, boolean disabled) { + int elementLineNumber = getPsiElementLineNumber(elt); + + var optionalSection = getListByLineNumber(builder, TargetSection.KEY, elementLineNumber); + + if (optionalSection.isEmpty()) { + return false; + } + + var listSection = optionalSection.get(); + + var targetsUpdater = ListSection.update(TargetSection.KEY, listSection); + + var targetStr = elt.getText(); + + targetsUpdater.replaceElement( + getPsiElementLineNumber(elt), + disabled ? + TargetExpression.fromStringSafe(targetStr.substring(1)) : + TargetExpression.fromStringSafe('-' + targetStr) + ); + + builder.replace(listSection, targetsUpdater); + + return true; + } + + private static > Optional getListByLineNumber(ProjectView.Builder builder, SectionKey key, int lineNumber) { + for (var section : builder.getAll(key)) { + if (section instanceof ListSection listSection && listSection.hasLineNumber(lineNumber)) { + return Optional.of(section); + } + } + + return Optional.empty(); + } + + private static int getPsiElementLineNumber(PsiElement elt) { + return Objects.requireNonNull( + FileDocumentManager + .getInstance() + .getDocument( + elt.getContainingFile() + .getVirtualFile() + ) + ).getLineNumber( + elt.getTextRange() + .getStartOffset() + ); + } +} diff --git a/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java b/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java index 0bfcd0f5687..043a3e81a89 100644 --- a/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java +++ b/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java @@ -88,6 +88,10 @@ public static Builder update( return new Builder<>(sectionKey, section); } + public boolean hasLineNumber(int elementLineNumber) { + return getFirstLineIndex() == elementLineNumber || itemsOrComments().stream().anyMatch(it ->it.getLineIndex() == elementLineNumber); + } + /** Builder for list sections */ public static class Builder extends SectionBuilder> { private final List> items = new ArrayList<>(); @@ -150,6 +154,18 @@ public final Builder removeAll(T item) { return this; } + @CanIgnoreReturnValue + public final Builder replaceElement(int lineIndex, T with) { + for (int i = 0; i < items.size(); i++) { + if (items.get(i).getLineIndex() == lineIndex) { + items.set(i, new ItemOrTextBlock<>(with, lineIndex)); + break; + } + } + + return this; + } + @Override public final ListSection build() { return new ListSection<>(getSectionKey(), ImmutableList.copyOf(items), firstLineNumber); diff --git a/base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java b/base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java index 51d073bbec7..9bbc61f2ed3 100644 --- a/base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java +++ b/base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java @@ -61,7 +61,9 @@ public final void print(StringBuilder sb, Section section) { if (divider != ' ') { sb.append(' '); } - printItem(sb, ((ScalarSection) section).getValue()); + var scalarSection = (ScalarSection) section; + + printItem(sb, scalarSection.getValue()); sb.append('\n'); } diff --git a/base/src/com/google/idea/blaze/base/projectview/section/Section.java b/base/src/com/google/idea/blaze/base/projectview/section/Section.java index dd5adc317bc..c4c5e204e70 100644 --- a/base/src/com/google/idea/blaze/base/projectview/section/Section.java +++ b/base/src/com/google/idea/blaze/base/projectview/section/Section.java @@ -19,7 +19,7 @@ import java.io.Serializable; /** - * A section is a part of an project view file. For instance: + * A section is a part of a project view file. For instance: * *

directories java/com/a java/com/b * diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlock.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlock.java index 59d9c7e6b86..01d083e1a74 100644 --- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlock.java +++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlock.java @@ -51,11 +51,16 @@ public static TextBlock newLine(int firstLineIndex) { return new TextBlock(ImmutableList.of(""), firstLineIndex); } - public void print(StringBuilder sb) { + @CanIgnoreReturnValue + public int print(StringBuilder sb) { + int addedLines = 0; for (String line : lines) { sb.append(line); sb.append('\n'); + addedLines += 1; } + + return addedLines; } @Override diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java index 64873e0a2f4..cd29ae89876 100644 --- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java +++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java @@ -131,6 +131,7 @@ private static TextBlock parseTextBlock(ParseContext parseContext, Pattern regex @Override public void print(StringBuilder sb, Section section) { TextBlockSection textBlockSection = (TextBlockSection) section; + textBlockSection.getTextBlock().print(sb); }