From 7a60186a4ba18a2a515d928558732ec15ec6fff5 Mon Sep 17 00:00:00 2001 From: junkfactory <2998269+junkfactory@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:44:40 -0700 Subject: [PATCH 1/4] adding builder annotation option UI --- .../ui/JavaInnerBuilderOption.java | 6 +- .../ui/JavaInnerBuilderOptionSelector.java | 67 +++++++++++++++---- .../innerbuilder/ui/TextAreaOption.java | 11 +++ 3 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/github/junkfactory/innerbuilder/ui/TextAreaOption.java diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java index 5061f1b..64e22f3 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java @@ -5,8 +5,10 @@ import java.util.Optional; public enum JavaInnerBuilderOption { - WITH_TO_BUILDER_METHOD("toBuilder", "Generate 'toBuilder()' method"), - WITH_VALIDATE_METHOD("validate", "Generate 'validate()' method"); + WITH_TO_BUILDER_METHOD("JavaInnerBuilderOption.toBuilder", "Generate 'toBuilder()' method"), + WITH_VALIDATE_METHOD("JavaInnerBuilderOption.validate", "Generate 'validate()' method"), + WITH_BUILDER_CLASS_ANNOTATIONS("JavaInnerBuilderOption.builderClassAnnotations", + "Generate annotations for the builder class"); private final String property; private final String description; diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java index ea95583..62d7804 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java @@ -7,10 +7,14 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.LabeledComponent; +import com.intellij.ui.JBColor; import com.intellij.ui.NonFocusableCheckBox; +import com.intellij.ui.border.CustomLineBorder; +import com.intellij.util.ui.JBUI; +import org.jetbrains.annotations.NotNull; import javax.swing.JComponent; -import java.awt.event.ItemEvent; +import javax.swing.JTextArea; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -33,6 +37,12 @@ public static Builder builder() { private List createGeneratorOptions() { var options = new ArrayList(); + options.add(new TextAreaOption( + JavaInnerBuilderOption.WITH_BUILDER_CLASS_ANNOTATIONS, + 5, + 40, + "Generate annotations for the builder class" + )); options.add(new CheckboxSelectorOption( JavaInnerBuilderOption.WITH_TO_BUILDER_METHOD, 'o' @@ -53,24 +63,47 @@ public List selectFieldsAndOptions() { return members; } - final JComponent[] optionCheckBoxes = buildOptions(); + final JComponent[] options = buildOptions(); final PsiFieldMember[] memberArray = members.toArray(new PsiFieldMember[0]); final MemberChooser chooser = new MemberChooser<>(memberArray, false, // allowEmptySelection true, // allowMultiSelection - project, null, optionCheckBoxes); + project, null, options); chooser.setTitle("Select Fields and Options for the Builder"); chooser.selectElements(memberArray); if (chooser.showAndGet()) { + setPropertyValuesFromOptions(chooser.getOptionControls()); return chooser.getSelectedElements(); } - return List.of(); } + private void setPropertyValuesFromOptions(JComponent[] options) { + var propertiesComponent = PropertiesComponent.getInstance(); + for (var option : options) { + if (option instanceof LabeledComponent labeledComponent && + labeledComponent.getComponent() instanceof JTextArea textArea) { + var textAreaOption = (JavaInnerBuilderOption) textArea.getClientProperty(JavaInnerBuilderOption.class); + propertiesComponent.setValue(textAreaOption.getProperty(), textArea.getText()); + } else if (option instanceof NonFocusableCheckBox checkBox) { + var checkboxSelectorOption = + (JavaInnerBuilderOption) option.getClientProperty(JavaInnerBuilderOption.class); + propertiesComponent.setValue(checkboxSelectorOption.getProperty(), + Boolean.toString(checkBox.isSelected())); + } else if (option instanceof LabeledComponent labeledComponent && + labeledComponent.getComponent() instanceof ComboBox comboBox) { + var selectorOption = (JavaInnerBuilderOption) option.getClientProperty(JavaInnerBuilderOption.class); + var selectedValue = (DropdownSelectorOptionValue) comboBox.getSelectedItem(); + if (null != selectedValue) { + propertiesComponent.setValue(selectorOption.getProperty(), selectedValue.option().getProperty()); + } + } + } + } + private JComponent[] buildOptions() { var propertiesComponent = PropertiesComponent.getInstance(); var options = createGeneratorOptions(); @@ -85,45 +118,51 @@ private JComponent[] buildOptions() { private JComponent buildOptions(PropertiesComponent propertiesComponent, SelectorOption selectorOption) { if (selectorOption instanceof CheckboxSelectorOption checkboxSelectorOption) { return buildCheckbox(propertiesComponent, checkboxSelectorOption); + } else if (selectorOption instanceof TextAreaOption textAreaOption) { + return buildTextArea(propertiesComponent, textAreaOption); } return buildDropdown(propertiesComponent, (DropdownSelectorOption) selectorOption); } + @NotNull + private LabeledComponent buildTextArea(PropertiesComponent propertiesComponent, + TextAreaOption textAreaOption) { + var textArea = new JTextArea(textAreaOption.numLines(), textAreaOption.numColumns()); + textArea.putClientProperty(JavaInnerBuilderOption.class, textAreaOption.option()); + textArea.setText(propertiesComponent.getValue(textAreaOption.option().getProperty())); + textArea.setBorder(new CustomLineBorder(JBColor.border(), JBUI.insets(1))); + var labeledComponent = LabeledComponent.create(textArea, textAreaOption.caption()); + labeledComponent.setToolTipText(textAreaOption.toolTip()); + return labeledComponent; + } + private JComponent buildCheckbox(PropertiesComponent propertiesComponent, CheckboxSelectorOption selectorOption) { var optionCheckBox = new NonFocusableCheckBox(selectorOption.caption()); + optionCheckBox.putClientProperty(JavaInnerBuilderOption.class, selectorOption.option()); optionCheckBox.setMnemonic(selectorOption.mnemonic()); optionCheckBox.setToolTipText(selectorOption.toolTip()); var optionProperty = selectorOption.option().getProperty(); optionCheckBox.setSelected(propertiesComponent.isTrueValue(optionProperty)); - optionCheckBox.addItemListener( - event -> propertiesComponent.setValue(optionProperty, Boolean.toString(optionCheckBox.isSelected()))); return optionCheckBox; } private JComponent buildDropdown(PropertiesComponent propertiesComponent, DropdownSelectorOption selectorOption) { final var comboBox = new ComboBox(); + comboBox.putClientProperty(JavaInnerBuilderOption.class, selectorOption.option()); comboBox.setEditable(false); comboBox.setRenderer(renderer); selectorOption.values().forEach(comboBox::addItem); comboBox.setSelectedItem(setSelectedComboBoxItem(propertiesComponent, selectorOption)); - comboBox.addItemListener(event -> setPropertiesComponentValue(propertiesComponent, selectorOption, event)); var labeledComponent = LabeledComponent.create(comboBox, selectorOption.caption()); labeledComponent.setToolTipText(selectorOption.toolTip()); - return labeledComponent; } - private void setPropertiesComponentValue(PropertiesComponent propertiesComponent, - DropdownSelectorOption selectorOption, ItemEvent itemEvent) { - var value = (DropdownSelectorOptionValue) itemEvent.getItem(); - propertiesComponent.setValue(selectorOption.option().getProperty(), value.option().getProperty()); - } - private DropdownSelectorOptionValue setSelectedComboBoxItem(PropertiesComponent propertiesComponent, DropdownSelectorOption selectorOption) { var selectedValue = propertiesComponent.getValue(selectorOption.option().getProperty()); diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/TextAreaOption.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/TextAreaOption.java new file mode 100644 index 0000000..c46e264 --- /dev/null +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/TextAreaOption.java @@ -0,0 +1,11 @@ +package com.github.junkfactory.innerbuilder.ui; + +public record TextAreaOption(JavaInnerBuilderOption option, + int numLines, + int numColumns, + String toolTip) implements SelectorOption { + @Override + public String caption() { + return option.getDescription(); + } +} From 20b5fd6d4428c71ecf45a7d7ed590b3b5f887f33 Mon Sep 17 00:00:00 2001 From: junkfactory <2998269+junkfactory@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:24:56 -0700 Subject: [PATCH 2/4] Hookup annotation options to Builder generation --- .../innerbuilder/JavaInnerBuilderHandler.java | 26 ++++--- .../generators/AbstractGenerator.java | 2 + .../generators/BuilderClassGenerator.java | 2 +- .../generators/BuilderMethodsGenerator.java | 2 - .../generators/GenerationResult.java | 5 ++ .../generators/InnerBuilderGenerator.java | 31 ++++++--- .../innerbuilder/generators/PsiParams.java | 12 +++- .../ui/JavaInnerBuilderOption.java | 28 ++++---- .../ui/JavaInnerBuilderOptionSelector.java | 69 ++++++++++++++++--- 9 files changed, 131 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java b/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java index 724ce22..1731a9d 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java @@ -17,6 +17,7 @@ import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.JavaCodeStyleManager; import com.intellij.util.AstLoadingFilter; import org.jetbrains.annotations.NotNull; @@ -91,6 +92,7 @@ public void invoke(@NotNull final Project project, @NotNull final Editor editor, .selectedFields(selectedFields) .factory(JavaPsiFacade.getElementFactory(project)) .codeStyleManager(JavaCodeStyleManager.getInstance(project)) + .psiManager(PsiManager.getInstance(project)) .build(); var generatorParams = GeneratorParams.builder() .project(project) @@ -108,17 +110,21 @@ private Set currentOptions() { final var options = EnumSet.noneOf(JavaInnerBuilderOption.class); final var propertiesComponent = PropertiesComponent.getInstance(); for (var option : JavaInnerBuilderOption.values()) { - - if (Boolean.TRUE.equals(option.isBooleanProperty())) { - final boolean currentSetting = propertiesComponent.getBoolean(option.getProperty(), false); - if (currentSetting) { - options.add(option); - } - } else { - String currentValue = String.valueOf(propertiesComponent.getValue(option.getProperty())); - if (currentValue != null) { + switch (option.getType()) { + case BOOLEAN: + if (propertiesComponent.getBoolean(option.getProperty(), false)) { + options.add(option); + } + break; + case LIST: + var list = propertiesComponent.getList(option.getProperty()); + if (null != list && !list.isEmpty()) { + options.add(option); + } + break; + default: + String currentValue = String.valueOf(propertiesComponent.getValue(option.getProperty())); JavaInnerBuilderOption.findValue(currentValue).ifPresent(options::add); - } } } return options; diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java index 47db588..46ca2d1 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java @@ -31,10 +31,12 @@ abstract class AbstractGenerator { protected final GeneratorFactory generatorFactory; protected final GeneratorParams generatorParams; + protected final GenerationResult generationResult; protected AbstractGenerator(GeneratorFactory generatorFactory, GeneratorParams generatorParams) { this.generatorFactory = generatorFactory; this.generatorParams = generatorParams; + this.generationResult = new GenerationResult(); } protected PsiElement addElement(PsiElement target, PsiElement element, PsiElement after) { diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderClassGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderClassGenerator.java index 176f7a3..86050a9 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderClassGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderClassGenerator.java @@ -27,7 +27,7 @@ public GenerationResult generate() { var methodsGenerator = generatorFactory.createBuilderMethodsGenerator(generatorParams, builderClassParams, fieldsGenerator); - return methodsGenerator.generate(); + return generationResult.merge(methodsGenerator.generate()); } private PsiMethod generateBuilderConstructor() { diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java index 102467f..064311d 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java @@ -16,7 +16,6 @@ class BuilderMethodsGenerator extends AbstractGenerator implements MethodsGenera private final BuilderClassParams builderClassParams; private final FieldsGenerator fieldsGenerator; - private final GenerationResult generationResult; private boolean isPublic; @@ -27,7 +26,6 @@ class BuilderMethodsGenerator extends AbstractGenerator implements MethodsGenera super(generatorFactory, generatorParams); this.builderClassParams = builderClassParams; this.fieldsGenerator = fieldsGenerator; - this.generationResult = new GenerationResult(); } @Override diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/GenerationResult.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/GenerationResult.java index a35a636..8c4286a 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/GenerationResult.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/GenerationResult.java @@ -34,4 +34,9 @@ public void when(Code code, Runnable runnable) { runnable.run(); } } + + public GenerationResult merge(GenerationResult other) { + result.or(other.result); + return this; + } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java index d422a73..075fd28 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java @@ -2,6 +2,7 @@ import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption; import com.intellij.codeInsight.generation.PsiFieldMember; +import com.intellij.ide.util.PropertiesComponent; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiField; import com.intellij.psi.PsiJavaFile; @@ -59,12 +60,13 @@ public GenerationResult generate() { .builderType(builderType) .build(); var result = generatorFactory.createBuilderClassGenerator(generatorParams, params).generate(); - + generationResult.merge(result); + targetClass.add(builderClass); var codeStyleManager = generatorParams.psi().codeStyleManager(); - result.when(ANNOTATIONS_ADDED, () -> codeStyleManager.shortenClassReferences(file)); - result.when(IMPORTS_ADDED, () -> codeStyleManager.removeRedundantImports((PsiJavaFile) file)); + generationResult.when(ANNOTATIONS_ADDED, () -> codeStyleManager.shortenClassReferences(targetClass)); + generationResult.when(IMPORTS_ADDED, () -> codeStyleManager.removeRedundantImports((PsiJavaFile) file)); CodeStyleManager.getInstance(generatorParams.project()).reformat(builderClass); - return result; + return generationResult; } private PsiMethod generateToBuilderMethod(PsiClass targetClass, @@ -162,11 +164,22 @@ private PsiClass findOrCreateBuilderClass(final PsiClass targetClass) { @NotNull private PsiClass createBuilderClass(final PsiClass targetClass) { - var builderClass = (PsiClass) targetClass.add(generatorParams.psi().factory() - .createClass(BUILDER_CLASS_NAME)); - PsiUtil.setModifierProperty(builderClass, PsiModifier.STATIC, true); - PsiUtil.setModifierProperty(builderClass, PsiModifier.FINAL, true); - return builderClass; + var classDef = new StringBuilder(); + if (generatorParams.options().contains(JavaInnerBuilderOption.WITH_BUILDER_CLASS_ANNOTATIONS)) { + var propertiesComponent = PropertiesComponent.getInstance(); + var annotationOptions = + propertiesComponent.getList(JavaInnerBuilderOption.WITH_BUILDER_CLASS_ANNOTATIONS.getProperty()); + if (annotationOptions != null) { + annotationOptions.forEach(a -> classDef.append('@').append(a).append(System.lineSeparator())); + generationResult.set(GenerationResult.Code.ANNOTATIONS_ADDED); + } + } + classDef.append("public static final class ") + .append(BUILDER_CLASS_NAME) + .append(" {}") + .append(System.lineSeparator()); + return generatorParams.psi().factory().createClassFromText(classDef.toString(), targetClass) + .getInnerClasses()[0]; } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/PsiParams.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/PsiParams.java index d1569b0..463632b 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/PsiParams.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/PsiParams.java @@ -3,6 +3,7 @@ import com.intellij.codeInsight.generation.PsiFieldMember; import com.intellij.psi.PsiElementFactory; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.JavaCodeStyleManager; import java.util.List; @@ -10,7 +11,8 @@ public record PsiParams(PsiFile file, List selectedFields, PsiElementFactory factory, - JavaCodeStyleManager codeStyleManager) { + JavaCodeStyleManager codeStyleManager, + PsiManager psiManager) { public static Builder builder() { return new Builder(); @@ -21,6 +23,7 @@ public static final class Builder { private List selectedFields; private PsiElementFactory factory; private JavaCodeStyleManager codeStyleManager; + private PsiManager psiManager; private Builder() { } @@ -45,8 +48,13 @@ public Builder codeStyleManager(JavaCodeStyleManager codeStyleManager) { return this; } + public Builder psiManager(PsiManager psiManager) { + this.psiManager = psiManager; + return this; + } + public PsiParams build() { - return new PsiParams(file, selectedFields, factory, codeStyleManager); + return new PsiParams(file, selectedFields, factory, codeStyleManager, psiManager); } } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java index 64e22f3..79ce7e8 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java @@ -5,31 +5,32 @@ import java.util.Optional; public enum JavaInnerBuilderOption { - WITH_TO_BUILDER_METHOD("JavaInnerBuilderOption.toBuilder", "Generate 'toBuilder()' method"), - WITH_VALIDATE_METHOD("JavaInnerBuilderOption.validate", "Generate 'validate()' method"), + WITH_TO_BUILDER_METHOD("JavaInnerBuilderOption.toBuilder", + "Generate 'toBuilder()' method", + Type.BOOLEAN), + WITH_VALIDATE_METHOD("JavaInnerBuilderOption.validate", + "Generate 'validate()' method", + Type.BOOLEAN), WITH_BUILDER_CLASS_ANNOTATIONS("JavaInnerBuilderOption.builderClassAnnotations", - "Generate annotations for the builder class"); + "Generate annotations for the builder class", + Type.LIST); private final String property; private final String description; - private final Boolean booleanProperty; + private final Type type; - JavaInnerBuilderOption(final String property, String description) { - this(property, description, true); - } - - JavaInnerBuilderOption(final String property, String description, final Boolean booleanProperty) { + JavaInnerBuilderOption(String property, String description, Type type) { this.property = String.format("JavaInnerBuilder.%s", property); this.description = description; - this.booleanProperty = booleanProperty; + this.type = type; } public String getProperty() { return property; } - public Boolean isBooleanProperty() { - return booleanProperty; + public Type getType() { + return type; } public String getDescription() { @@ -42,4 +43,7 @@ public static Optional findValue(String value) { .findFirst(); } + public enum Type { + BOOLEAN, LIST + } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java index 62d7804..6864437 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java @@ -7,6 +7,9 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.LabeledComponent; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.PsiManager; +import com.intellij.psi.util.ClassUtil; import com.intellij.ui.JBColor; import com.intellij.ui.NonFocusableCheckBox; import com.intellij.ui.border.CustomLineBorder; @@ -37,12 +40,6 @@ public static Builder builder() { private List createGeneratorOptions() { var options = new ArrayList(); - options.add(new TextAreaOption( - JavaInnerBuilderOption.WITH_BUILDER_CLASS_ANNOTATIONS, - 5, - 40, - "Generate annotations for the builder class" - )); options.add(new CheckboxSelectorOption( JavaInnerBuilderOption.WITH_TO_BUILDER_METHOD, 'o' @@ -51,6 +48,12 @@ private List createGeneratorOptions() { JavaInnerBuilderOption.WITH_VALIDATE_METHOD, 'v' )); + options.add(new TextAreaOption( + JavaInnerBuilderOption.WITH_BUILDER_CLASS_ANNOTATIONS, + 5, + 40, + "Use fully qualified class names separated by new lines." + )); return options; } @@ -67,15 +70,26 @@ public List selectFieldsAndOptions() { final PsiFieldMember[] memberArray = members.toArray(new PsiFieldMember[0]); + while (true) { + try { + return showOptionsDialog(memberArray, options); + } catch (IllegalArgumentException e) { + Messages.showErrorDialog(e.getMessage(), "Invalid Options"); + } + } + } + + private List showOptionsDialog(PsiFieldMember[] memberArray, JComponent[] optionsArray) { final MemberChooser chooser = new MemberChooser<>(memberArray, false, // allowEmptySelection true, // allowMultiSelection - project, null, options); + project, null, optionsArray); chooser.setTitle("Select Fields and Options for the Builder"); chooser.selectElements(memberArray); if (chooser.showAndGet()) { - setPropertyValuesFromOptions(chooser.getOptionControls()); + var optionControls = chooser.getOptionControls(); + setPropertyValuesFromOptions(optionControls); return chooser.getSelectedElements(); } return List.of(); @@ -86,8 +100,9 @@ private void setPropertyValuesFromOptions(JComponent[] options) { for (var option : options) { if (option instanceof LabeledComponent labeledComponent && labeledComponent.getComponent() instanceof JTextArea textArea) { + var annotations = getAnnotations(textArea); var textAreaOption = (JavaInnerBuilderOption) textArea.getClientProperty(JavaInnerBuilderOption.class); - propertiesComponent.setValue(textAreaOption.getProperty(), textArea.getText()); + propertiesComponent.setList(textAreaOption.getProperty(), annotations); } else if (option instanceof NonFocusableCheckBox checkBox) { var checkboxSelectorOption = (JavaInnerBuilderOption) option.getClientProperty(JavaInnerBuilderOption.class); @@ -104,6 +119,33 @@ private void setPropertyValuesFromOptions(JComponent[] options) { } } + private List getAnnotations(JTextArea textArea) { + var annotations = textArea.getText(); + if (annotations.isBlank()) { + return List.of(); + } + var psiManager = PsiManager.getInstance(project); + var errors = new StringBuilder(); + var lines = annotations.split("\n"); + for (var line : lines) { + if (line.isBlank()) { + continue; + } + if (line.charAt(0) == '@') { + line = line.substring(1); + } + var psiClass = ClassUtil.findPsiClass(psiManager, line.trim()); + if (null == psiClass) { + errors.append(" - ").append(line).append(System.lineSeparator()); + } + } + if (!errors.isEmpty()) { + throw new IllegalArgumentException(errors.insert(0, "Invalid Annotations:\n").toString()); + } + return List.of(lines); + } + + private JComponent[] buildOptions() { var propertiesComponent = PropertiesComponent.getInstance(); var options = createGeneratorOptions(); @@ -129,7 +171,14 @@ private LabeledComponent buildTextArea(PropertiesComponent properties TextAreaOption textAreaOption) { var textArea = new JTextArea(textAreaOption.numLines(), textAreaOption.numColumns()); textArea.putClientProperty(JavaInnerBuilderOption.class, textAreaOption.option()); - textArea.setText(propertiesComponent.getValue(textAreaOption.option().getProperty())); + if (textAreaOption.option().getType() == JavaInnerBuilderOption.Type.LIST) { + var annotations = propertiesComponent.getList(textAreaOption.option().getProperty()); + if (null != annotations) { + textArea.setText(String.join("\n", annotations)); + } + } else { + textArea.setText(propertiesComponent.getValue(textAreaOption.option().getProperty())); + } textArea.setBorder(new CustomLineBorder(JBColor.border(), JBUI.insets(1))); var labeledComponent = LabeledComponent.create(textArea, textAreaOption.caption()); labeledComponent.setToolTipText(textAreaOption.toolTip()); From bf6f1af615bc15068c65cb5b8fe6aceadcfd6e4d Mon Sep 17 00:00:00 2001 From: junkfactory <2998269+junkfactory@users.noreply.github.com> Date: Sat, 10 Aug 2024 21:08:56 -0700 Subject: [PATCH 3/4] Add validation of annotations --- .../innerbuilder/generators/Utils.java | 12 +++ .../ui/JavaInnerBuilderOption.java | 48 +++++++++- .../ui/JavaInnerBuilderOptionSelector.java | 88 ++++++------------- .../ui/OptionValidatorFactory.java | 11 +++ .../ui/ValidatingFieldMemberChooser.java | 37 ++++++++ 5 files changed, 132 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/github/junkfactory/innerbuilder/ui/OptionValidatorFactory.java create mode 100644 src/main/java/com/github/junkfactory/innerbuilder/ui/ValidatingFieldMemberChooser.java diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java index 0d0e621..df3608f 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java @@ -14,7 +14,10 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.function.Predicate; public class Utils { @NonNls @@ -134,5 +137,14 @@ public static boolean isFieldInitializedWithImmutableInstance(PsiField field) { return null != initializerClass && initializerClass.hasModifierProperty(PsiModifier.ABSTRACT); } + public static List stringToList(String str) { + if (null == str || str.isBlank()) { + return List.of(); + } + return Arrays.stream(str.split(System.lineSeparator())) + .map(String::trim) + .filter(Predicate.not(String::isBlank)) + .toList(); + } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java index 79ce7e8..c86fc7b 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java @@ -1,5 +1,18 @@ package com.github.junkfactory.innerbuilder.ui; +import com.github.junkfactory.innerbuilder.generators.Utils; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.ui.ComponentValidator; +import com.intellij.openapi.ui.LabeledComponent; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.psi.PsiManager; +import com.intellij.psi.util.ClassUtil; +import com.intellij.ui.JBColor; +import com.intellij.ui.border.CustomLineBorder; +import com.intellij.util.ui.JBUI; + +import javax.swing.JComponent; +import javax.swing.JTextArea; import java.util.Arrays; import java.util.Objects; import java.util.Optional; @@ -13,16 +26,42 @@ public enum JavaInnerBuilderOption { Type.BOOLEAN), WITH_BUILDER_CLASS_ANNOTATIONS("JavaInnerBuilderOption.builderClassAnnotations", "Generate annotations for the builder class", - Type.LIST); + Type.LIST, + (p, d, j) -> new ComponentValidator(d).withValidator(() -> { + if (j instanceof JTextArea textArea) { + var errors = new StringBuilder(); + var annotations = Utils.stringToList(textArea.getText()); + for (var annotation : annotations) { + if (ClassUtil.findPsiClass(p, annotation) == null) { + errors.append(" - ").append(annotation).append("\n"); + } + } + if (!errors.isEmpty()) { + textArea.setBorder(new CustomLineBorder(JBColor.RED, JBUI.insets(1))); + return new ValidationInfo(errors.insert(0, "Annotations not found") + .append(System.lineSeparator()).toString(), textArea); + } + textArea.setBorder(new CustomLineBorder(JBColor.border(), JBUI.insets(1))); + } + return null; + }).installOn(j)); private final String property; private final String description; private final Type type; + private final OptionValidatorFactory validatorFactory; JavaInnerBuilderOption(String property, String description, Type type) { + this(property, description, type, (p, d, j) -> { + }); + } + + JavaInnerBuilderOption(String property, String description, Type type, + OptionValidatorFactory validatorFactory) { this.property = String.format("JavaInnerBuilder.%s", property); this.description = description; this.type = type; + this.validatorFactory = validatorFactory; } public String getProperty() { @@ -37,6 +76,13 @@ public String getDescription() { return description; } + public void createValidator(PsiManager psiManager, Disposable disposable, JComponent component) { + if (component instanceof LabeledComponent labeledComponent) { + component = labeledComponent.getComponent(); + } + validatorFactory.create(psiManager, disposable, component); + } + public static Optional findValue(String value) { return Arrays.stream(values()) .filter(it -> Objects.equals(it.getProperty(), value)) diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java index 6864437..318405f 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOptionSelector.java @@ -1,15 +1,13 @@ package com.github.junkfactory.innerbuilder.ui; +import com.github.junkfactory.innerbuilder.generators.Utils; import com.intellij.codeInsight.generation.PsiFieldMember; -import com.intellij.ide.util.MemberChooser; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.LabeledComponent; -import com.intellij.openapi.ui.Messages; import com.intellij.psi.PsiManager; -import com.intellij.psi.util.ClassUtil; import com.intellij.ui.JBColor; import com.intellij.ui.NonFocusableCheckBox; import com.intellij.ui.border.CustomLineBorder; @@ -66,24 +64,18 @@ public List selectFieldsAndOptions() { return members; } - final JComponent[] options = buildOptions(); - - final PsiFieldMember[] memberArray = members.toArray(new PsiFieldMember[0]); - - while (true) { - try { - return showOptionsDialog(memberArray, options); - } catch (IllegalArgumentException e) { - Messages.showErrorDialog(e.getMessage(), "Invalid Options"); - } - } - } - - private List showOptionsDialog(PsiFieldMember[] memberArray, JComponent[] optionsArray) { - final MemberChooser chooser = new MemberChooser<>(memberArray, + var optionsArray = buildOptions(); + var memberArray = members.toArray(new PsiFieldMember[0]); + var chooser = new ValidatingFieldMemberChooser(memberArray, false, // allowEmptySelection true, // allowMultiSelection - project, null, optionsArray); + project, null, optionsArray) { + }; + for (var optionComponent : optionsArray) { + var builderOption = (JavaInnerBuilderOption) optionComponent.getClientProperty( + JavaInnerBuilderOption.class); + builderOption.createValidator(PsiManager.getInstance(project), chooser.getDisposable(), optionComponent); + } chooser.setTitle("Select Fields and Options for the Builder"); chooser.selectElements(memberArray); @@ -95,64 +87,37 @@ private List showOptionsDialog(PsiFieldMember[] memberArray, JCo return List.of(); } - private void setPropertyValuesFromOptions(JComponent[] options) { + private void setPropertyValuesFromOptions(JComponent[] optionComponents) { var propertiesComponent = PropertiesComponent.getInstance(); - for (var option : options) { - if (option instanceof LabeledComponent labeledComponent && + for (var component : optionComponents) { + var option = (JavaInnerBuilderOption) component.getClientProperty(JavaInnerBuilderOption.class); + if (component instanceof LabeledComponent labeledComponent && labeledComponent.getComponent() instanceof JTextArea textArea) { - var annotations = getAnnotations(textArea); - var textAreaOption = (JavaInnerBuilderOption) textArea.getClientProperty(JavaInnerBuilderOption.class); - propertiesComponent.setList(textAreaOption.getProperty(), annotations); - } else if (option instanceof NonFocusableCheckBox checkBox) { - var checkboxSelectorOption = - (JavaInnerBuilderOption) option.getClientProperty(JavaInnerBuilderOption.class); - propertiesComponent.setValue(checkboxSelectorOption.getProperty(), + var annotations = Utils.stringToList(textArea.getText()); + propertiesComponent.setList(option.getProperty(), annotations); + } else if (component instanceof NonFocusableCheckBox checkBox) { + propertiesComponent.setValue(option.getProperty(), Boolean.toString(checkBox.isSelected())); - } else if (option instanceof LabeledComponent labeledComponent && + } else if (component instanceof LabeledComponent labeledComponent && labeledComponent.getComponent() instanceof ComboBox comboBox) { - var selectorOption = (JavaInnerBuilderOption) option.getClientProperty(JavaInnerBuilderOption.class); var selectedValue = (DropdownSelectorOptionValue) comboBox.getSelectedItem(); if (null != selectedValue) { - propertiesComponent.setValue(selectorOption.getProperty(), selectedValue.option().getProperty()); + propertiesComponent.setValue(option.getProperty(), selectedValue.option().getProperty()); } } } } - private List getAnnotations(JTextArea textArea) { - var annotations = textArea.getText(); - if (annotations.isBlank()) { - return List.of(); - } - var psiManager = PsiManager.getInstance(project); - var errors = new StringBuilder(); - var lines = annotations.split("\n"); - for (var line : lines) { - if (line.isBlank()) { - continue; - } - if (line.charAt(0) == '@') { - line = line.substring(1); - } - var psiClass = ClassUtil.findPsiClass(psiManager, line.trim()); - if (null == psiClass) { - errors.append(" - ").append(line).append(System.lineSeparator()); - } - } - if (!errors.isEmpty()) { - throw new IllegalArgumentException(errors.insert(0, "Invalid Annotations:\n").toString()); - } - return List.of(lines); - } - - private JComponent[] buildOptions() { var propertiesComponent = PropertiesComponent.getInstance(); var options = createGeneratorOptions(); var optionCount = options.size(); var checkBoxesArray = new JComponent[optionCount]; for (int i = 0; i < optionCount; i++) { - checkBoxesArray[i] = buildOptions(propertiesComponent, options.get(i)); + var option = options.get(i); + var optionComponent = buildOptions(propertiesComponent, option); + optionComponent.putClientProperty(JavaInnerBuilderOption.class, option.option()); + checkBoxesArray[i] = optionComponent; } return checkBoxesArray; } @@ -170,7 +135,6 @@ private JComponent buildOptions(PropertiesComponent propertiesComponent, Selecto private LabeledComponent buildTextArea(PropertiesComponent propertiesComponent, TextAreaOption textAreaOption) { var textArea = new JTextArea(textAreaOption.numLines(), textAreaOption.numColumns()); - textArea.putClientProperty(JavaInnerBuilderOption.class, textAreaOption.option()); if (textAreaOption.option().getType() == JavaInnerBuilderOption.Type.LIST) { var annotations = propertiesComponent.getList(textAreaOption.option().getProperty()); if (null != annotations) { @@ -188,7 +152,6 @@ private LabeledComponent buildTextArea(PropertiesComponent properties private JComponent buildCheckbox(PropertiesComponent propertiesComponent, CheckboxSelectorOption selectorOption) { var optionCheckBox = new NonFocusableCheckBox(selectorOption.caption()); - optionCheckBox.putClientProperty(JavaInnerBuilderOption.class, selectorOption.option()); optionCheckBox.setMnemonic(selectorOption.mnemonic()); optionCheckBox.setToolTipText(selectorOption.toolTip()); @@ -200,7 +163,6 @@ private JComponent buildCheckbox(PropertiesComponent propertiesComponent, private JComponent buildDropdown(PropertiesComponent propertiesComponent, DropdownSelectorOption selectorOption) { final var comboBox = new ComboBox(); - comboBox.putClientProperty(JavaInnerBuilderOption.class, selectorOption.option()); comboBox.setEditable(false); comboBox.setRenderer(renderer); selectorOption.values().forEach(comboBox::addItem); diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/OptionValidatorFactory.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/OptionValidatorFactory.java new file mode 100644 index 0000000..8af5f30 --- /dev/null +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/OptionValidatorFactory.java @@ -0,0 +1,11 @@ +package com.github.junkfactory.innerbuilder.ui; + +import com.intellij.openapi.Disposable; +import com.intellij.psi.PsiManager; + +import javax.swing.JComponent; + +@FunctionalInterface +interface OptionValidatorFactory { + void create(PsiManager psiManager, Disposable disposable, JComponent optionComponent); +} diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/ValidatingFieldMemberChooser.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/ValidatingFieldMemberChooser.java new file mode 100644 index 0000000..3fad365 --- /dev/null +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/ValidatingFieldMemberChooser.java @@ -0,0 +1,37 @@ +package com.github.junkfactory.innerbuilder.ui; + +import com.intellij.codeInsight.generation.PsiFieldMember; +import com.intellij.ide.util.MemberChooser; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComponentValidator; +import com.intellij.openapi.ui.LabeledComponent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.JComponent; + +class ValidatingFieldMemberChooser extends MemberChooser { + ValidatingFieldMemberChooser(PsiFieldMember[] elements, boolean allowEmptySelection, boolean allowMultiSelection, + @NotNull Project project, @Nullable JComponent headerPanel, + JComponent[] optionControls) { + super(elements, allowEmptySelection, allowMultiSelection, project, headerPanel, optionControls); + } + + @Override + protected void doOKAction() { + boolean hasErrors = false; + for (var optionComponent : getOptionControls()) { + optionComponent = optionComponent instanceof LabeledComponent labeledComponent ? + labeledComponent.getComponent() : optionComponent; + var optionalValidator = ComponentValidator.getInstance(optionComponent); + if (optionalValidator.isPresent()) { + var validator = optionalValidator.get(); + validator.revalidate(); + hasErrors |= null != validator.getValidationInfo(); + } + } + if (!hasErrors) { + super.doOKAction(); + } + } +} From dc2245007b349e0507d129648df435ed40e9124b Mon Sep 17 00:00:00 2001 From: junkfactory <2998269+junkfactory@users.noreply.github.com> Date: Sat, 10 Aug 2024 21:34:15 -0700 Subject: [PATCH 4/4] Fix type parsing for annotations with parameters --- .../github/junkfactory/innerbuilder/generators/Utils.java | 5 +++++ .../junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java index df3608f..097574a 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/Utils.java @@ -147,4 +147,9 @@ public static List stringToList(String str) { .toList(); } + public static String parseType(String text) { + var parenthesisIndex = text.indexOf('('); + return parenthesisIndex == -1 ? text : text.substring(0, parenthesisIndex); + } + } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java index c86fc7b..b5740cd 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/ui/JavaInnerBuilderOption.java @@ -31,7 +31,8 @@ public enum JavaInnerBuilderOption { if (j instanceof JTextArea textArea) { var errors = new StringBuilder(); var annotations = Utils.stringToList(textArea.getText()); - for (var annotation : annotations) { + for (var annotationText : annotations) { + var annotation = Utils.parseType(annotationText); if (ClassUtil.findPsiClass(p, annotation) == null) { errors.append(" - ").append(annotation).append("\n"); }