diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java index 0e225c06b..9529bba9d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java @@ -1,17 +1,11 @@ package software.coley.recaf.services.cell.text; -import dev.xdark.blw.asm.internal.Util; -import dev.xdark.blw.code.Instruction; -import dev.xdark.blw.code.instruction.*; import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import me.darknet.assembler.helper.Names; -import me.darknet.assembler.printer.InstructionPrinter; -import me.darknet.assembler.printer.PrintContext; import org.benf.cfr.reader.entities.annotations.ElementValue; import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.AbstractInsnNode; import software.coley.recaf.info.*; import software.coley.recaf.info.annotation.Annotated; import software.coley.recaf.info.annotation.AnnotationElement; @@ -22,6 +16,7 @@ import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.services.Service; import software.coley.recaf.services.phantom.GeneratedPhantomWorkspaceResource; +import software.coley.recaf.ui.config.MemberDisplayFormatConfig; import software.coley.recaf.ui.config.TextFormatConfig; import software.coley.recaf.ui.control.tree.WorkspaceTreeCell; import software.coley.recaf.util.BlwUtil; @@ -32,7 +27,6 @@ import software.coley.recaf.workspace.model.bundle.*; import software.coley.recaf.workspace.model.resource.*; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -47,12 +41,15 @@ public class TextProviderService implements Service { public static final String SERVICE_ID = "cell-text"; private final TextProviderServiceConfig config; private final TextFormatConfig formatConfig; + private final MemberDisplayFormatConfig memberFormatConfig; @Inject public TextProviderService(@Nonnull TextProviderServiceConfig config, - @Nonnull TextFormatConfig formatConfig) { + @Nonnull TextFormatConfig formatConfig, + @Nonnull MemberDisplayFormatConfig memberFormatConfig) { this.config = config; this.formatConfig = formatConfig; + this.memberFormatConfig = memberFormatConfig; // Unlike the other services for graphics/menus, I don't see a use-case for text customization... // Will keep the model similar to them though just in case so that it is easy to add in the future. } @@ -185,10 +182,7 @@ public TextProvider getFieldMemberTextProvider(@Nonnull Workspace workspace, @Nonnull ClassBundle bundle, @Nonnull ClassInfo declaringClass, @Nonnull FieldMember field) { - // TODO: Will want to provide config option for showing the type - // - name (default) - // - type + name - return () -> formatConfig.filter(field.getName()); + return () -> formatConfig.filter(memberFormatConfig.getDisplay(field), false, true, true); } /** @@ -211,11 +205,7 @@ public TextProvider getMethodMemberTextProvider(@Nonnull Workspace workspace, @Nonnull ClassBundle bundle, @Nonnull ClassInfo declaringClass, @Nonnull MethodMember method) { - // TODO: Will want to provide config option for showing the descriptor - // - hidden (default) - // - raw - // - simple names - return () -> formatConfig.filter(method.getName()); + return () -> formatConfig.filter(memberFormatConfig.getDisplay(method), false, true, true); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/config/MemberDisplayFormatConfig.java b/recaf-ui/src/main/java/software/coley/recaf/ui/config/MemberDisplayFormatConfig.java new file mode 100644 index 000000000..a647745fc --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/config/MemberDisplayFormatConfig.java @@ -0,0 +1,72 @@ +package software.coley.recaf.ui.config; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.objectweb.asm.Type; +import software.coley.observables.ObservableObject; +import software.coley.recaf.config.BasicConfigContainer; +import software.coley.recaf.config.BasicConfigValue; +import software.coley.recaf.config.ConfigGroups; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.util.Types; + +/** + * Config for {@link ClassMember} display. + * + * @author Matt Coley + */ +@ApplicationScoped +public class MemberDisplayFormatConfig extends BasicConfigContainer { + public static final String ID = "member-format"; + private final ObservableObject nameTypeDisplay = new ObservableObject<>(Display.NAME_ONLY); + + @Inject + public MemberDisplayFormatConfig() { + super(ConfigGroups.SERVICE_UI, ID + CONFIG_SUFFIX); + // Add values + addValue(new BasicConfigValue<>("name-type-display", Display.class, nameTypeDisplay)); + } + + @Nonnull + public ObservableObject getNameTypeDisplay() { + return nameTypeDisplay; + } + + @Nonnull + public String getDisplay(@Nonnull ClassMember member) { + if (member instanceof FieldMember field) + return getDisplay(field); + else if (member instanceof MethodMember method) + return getDisplay(method); + throw new IllegalStateException("Member not field or method: " + member); + } + + @Nonnull + public String getDisplay(@Nonnull FieldMember member) { + return switch (nameTypeDisplay.getValue()) { + case NAME_ONLY -> member.getName(); + case NAME_AND_RAW_DESCRIPTOR -> member.getName() + " " + member.getDescriptor(); + case NAME_AND_PRETTY_DESCRIPTOR -> + member.getName() + " " + Types.pretty(Type.getType(member.getDescriptor())); + }; + } + + @Nonnull + public String getDisplay(@Nonnull MethodMember member) { + return switch (nameTypeDisplay.getValue()) { + case NAME_ONLY -> member.getName(); + case NAME_AND_RAW_DESCRIPTOR -> member.getName() + member.getDescriptor(); + case NAME_AND_PRETTY_DESCRIPTOR -> + member.getName() + " " + Types.pretty(Type.getMethodType(member.getDescriptor())); + }; + } + + public enum Display { + NAME_ONLY, + NAME_AND_RAW_DESCRIPTOR, + NAME_AND_PRETTY_DESCRIPTOR + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java b/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java index 8aa1084b1..70e87b9bb 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java @@ -45,7 +45,7 @@ public ObservableBoolean getDoEscape() { * @return {@code true} to shorten path text with {@link #filter(String)}. */ @Nonnull - public ObservableBoolean getDoShorten() { + public ObservableBoolean getDoShortenPaths() { return shorten; } @@ -64,10 +64,26 @@ public ObservableInteger getMaxLength() { * @return Filtered text based on current config. */ public String filter(@Nullable String string) { + return filter(string, true, true, true); + } + + /** + * @param string + * Some text to filter. + * @param shortenPath + * Apply path shortening filtering. + * @param escape + * Apply escaping. + * @param maxLength + * Apply max length cap. + * + * @return Filtered text based on current config. + */ + public String filter(@Nullable String string, boolean shortenPath, boolean escape, boolean maxLength) { if (string == null) return null; - string = filterShorten(string); - string = filterEscape(string); - string = filterMaxLength(string); + if (shortenPath) string = filterShorten(string); + if (escape) string = filterEscape(string); + if (maxLength) string = filterMaxLength(string); return string; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java index a567d0439..765370bbb 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java @@ -125,6 +125,17 @@ public List getProblemsByLevel(ProblemLevel level) { return getProblems(p -> p.getLevel() == level); } + /** + * @param phase + * Problem phase to filter problems by. + * + * @return List of problems matching the given phase. + */ + @Nonnull + public List getProblemsByPhase(ProblemPhase phase) { + return getProblems(p -> p.getPhase() == phase); + } + /** * @param filter * Filter to pass problems through. diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java index eb38da4d7..f01875701 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.util.ReflectUtil; +import software.coley.recaf.util.Unchecked; import java.lang.reflect.Field; import java.util.Collections; @@ -33,6 +34,7 @@ public class FilterableTreeItem extends TreeItem { private static final Field CHILDREN_FIELD; private static final Logger logger = Logging.get(FilterableTreeItem.class); private final ObservableList> sourceChildren = FXCollections.observableArrayList(); + private final ObjectProperty> sourceParent = new SimpleObjectProperty<>(); private final ObjectProperty>> predicate = new SimpleObjectProperty<>(); protected FilterableTreeItem() { @@ -62,6 +64,14 @@ public ObservableList> getChildren() { return super.getChildren(); } + /** + * @return Source parent, ignoring filtering. + */ + @Nonnull + public ObjectProperty> sourceParentProperty() { + return sourceParent; + } + /** * @return {@code true} when the item MUST be shown. */ @@ -135,6 +145,8 @@ public void addAndSortChild(@Nonnull TreeItem item) { index = -(index + 1); sourceChildren.add(index, item); } + if (item instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(Unchecked.cast(this)); } } @@ -146,6 +158,8 @@ public void addAndSortChild(@Nonnull TreeItem item) { */ protected void addPreSortedChild(@Nonnull TreeItem item) { sourceChildren.add(item); + if (item instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(Unchecked.cast(this)); } /** @@ -159,6 +173,8 @@ protected void addPreSortedChild(@Nonnull TreeItem item) { */ public boolean removeSourceChild(@Nonnull TreeItem child) { synchronized (sourceChildren) { + if (child instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(null); return sourceChildren.remove(child); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java index 20fbd38f0..ce13f2f99 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java @@ -1,5 +1,6 @@ package software.coley.recaf.ui.control.tree; +import jakarta.annotation.Nonnull; import javafx.scene.control.TextField; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -24,7 +25,7 @@ public class TreeFiltering { * Assumed that tree contents are {@link FilterableTreeItem}. */ @SuppressWarnings({"unchecked", "rawtypes"}) - public static void install(TextField filter, TreeView tree) { + public static void install(@Nonnull TextField filter, @Nonnull TreeView tree) { NodeEvents.addKeyPressHandler(filter, e -> { if (e.getCode() == KeyCode.ESCAPE) { filter.clear(); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java index 6de0e3836..ac4b23b8b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java @@ -1,6 +1,7 @@ package software.coley.recaf.ui.control.tree; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -16,7 +17,7 @@ public class TreeItems { * Expand all parents to this item. */ public static void expandParents(@Nonnull TreeItem item) { - while ((item = item.getParent()) != null) + while ((item = getParent(item)) != null) item.setExpanded(true); } @@ -62,4 +63,18 @@ private static void recurseClose(@Nonnull TreeItem item) { item.getChildren().forEach(TreeItems::recurseClose); } } + + /** + * @param item + * Tree item to get parent of. + * + * @return Parent tree item. + */ + @Nullable + private static TreeItem getParent(@Nonnull TreeItem item) { + TreeItem parent = item.getParent(); + if (parent == null && item instanceof FilterableTreeItem filterableItem) + parent = filterableItem.sourceParentProperty().get(); + return parent; + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java index 90dcab76e..dafc3cd6d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java @@ -1,13 +1,18 @@ package software.coley.recaf.ui.control.tree; +import atlantafx.base.controls.CustomTextField; +import atlantafx.base.theme.Styles; import jakarta.annotation.Nonnull; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; +import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.path.DirectoryPathNode; import software.coley.recaf.path.FilePathNode; import software.coley.recaf.path.PathNode; -import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.ui.control.BoundToggleIcon; +import software.coley.recaf.ui.control.FontIconView; import software.coley.recaf.util.Lang; /** @@ -16,45 +21,54 @@ * @author Matt Coley */ public class WorkspaceTreeFilterPane extends BorderPane { - private final TextField textField = new TextField(); + private final SimpleBooleanProperty caseSensitivity = new SimpleBooleanProperty(false); + private final CustomTextField textField = new CustomTextField(); /** * @param tree * Tree to filter. */ public WorkspaceTreeFilterPane(@Nonnull WorkspaceTree tree) { + BoundToggleIcon toggleSensitivity = new BoundToggleIcon(new FontIconView(CarbonIcons.LETTER_CC), caseSensitivity).withTooltip("misc.casesensitive"); + toggleSensitivity.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.ACCENT, Styles.FLAT, Styles.SMALL); + textField.rightProperty().set(toggleSensitivity); + textField.promptTextProperty().bind(Lang.getBinding("workspace.filter-prompt")); setCenter(textField); getStyleClass().add("workspace-filter-pane"); textField.getStyleClass().add("workspace-filter-text"); - // TODO: - // - option to hide supporting resources - // - case sensitivity toggle - // Setup tree item predicate property on FX thread. - // The root is assigned on the FX thread, it won't be available if we call it immediately. - FxThreadUtil.run(() -> { - // We're not binding from the root's property since that will trigger immediately. - // That will force-expand the entire workspace, which we do not want to do. - textField.textProperty().addListener((ob, old, cur) -> { - WorkspaceTreeNode root = (WorkspaceTreeNode) tree.getRoot(); - root.predicateProperty().set(item -> { - String path; - PathNode node = item.getValue(); - if (node instanceof DirectoryPathNode directoryNode) { - path = directoryNode.getValue(); - } else if (node instanceof ClassPathNode classPathNode) { - path = classPathNode.getValue().getName(); - } else if (node instanceof FilePathNode classPathNode) { - path = classPathNode.getValue().getName(); - } else { - path = null; - } - return path == null || path.contains(cur); - }); + textField.textProperty().addListener((ob, old, cur) -> update(tree)); + caseSensitivity.addListener((ob, old, cur) -> update(tree)); + } + + private void update(@Nonnull WorkspaceTree tree) { + WorkspaceTreeNode root = (WorkspaceTreeNode) tree.getRoot(); + if (root == null) return; + + if (textField.getText().isEmpty()) + root.predicateProperty().set(null); + else + root.predicateProperty().set(item -> { + String path; + PathNode node = item.getValue(); + if (node instanceof DirectoryPathNode directoryNode) { + path = directoryNode.getValue(); + } else if (node instanceof ClassPathNode classPathNode) { + path = classPathNode.getValue().getName(); + } else if (node instanceof FilePathNode classPathNode) { + path = classPathNode.getValue().getName(); + } else { + path = null; + } + + if (path == null) return true; + + return caseSensitivity.get() ? + path.contains(textField.getText()) : + path.toLowerCase().contains(textField.getText().toLowerCase()); }); - }); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java index dbbf71e60..d78d5197a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java @@ -8,7 +8,7 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.control.Separator; +import javafx.scene.control.TitledPane; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; @@ -28,6 +28,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -43,22 +44,23 @@ public class WorkspaceInformationPane extends BorderPane implements Navigable { @Inject public WorkspaceInformationPane(@Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull ResourceSummaryService summaryService, - @Nonnull Workspace workspace) { + @Nonnull IconProviderService iconService, + @Nonnull ResourceSummaryService summaryService, + @Nonnull Workspace workspace) { path = PathNodes.workspacePath(workspace); // Adding content Grid content = new Grid(); - content.setPadding(new Insets(5)); - content.prefWidthProperty().bind(widthProperty()); + content.setPadding(new Insets(10)); + content.prefWidthProperty().bind(widthProperty().subtract(10)); ScrollPane scroll = new ScrollPane(content); setCenter(scroll); getStyleClass().add("background"); // Populate summary data for each resource. - for (WorkspaceResource resource : workspace.getAllResources(false)) { - // Add header + List resources = workspace.getAllResources(false); + for (WorkspaceResource resource : resources) { + // Create header. Node graphic = iconService.getResourceIconProvider(workspace, resource).makeIcon(); Label title = new Label(textService.getResourceTextProvider(workspace, resource).makeText()); Label subtitle = new Label(String.format("%d classes, %d files", @@ -68,14 +70,21 @@ public WorkspaceInformationPane(@Nonnull TextProviderService textService, title.getStyleClass().add(Styles.TITLE_4); title.setGraphic(graphic); subtitle.getStyleClass().add(Styles.TEXT_SUBTLE); - VBox wrapper = new VBox(title, subtitle); - content.add(wrapper, 0, content.getRowCount(), 2, 2); - // Add summaries - summaryService.summarizeTo(workspace, resource, content); - - // Break each summary by newline - content.add(new Separator(), 0, content.getRowCount(), 2, 1); + if (resources.size() > 1) { + // Add summaries for this resource into a collapsible panel. + Grid section = content.newSection(); + TitledPane resourcePane = new TitledPane(); + resourcePane.setContent(section); + resourcePane.setGraphic(new VBox(title, subtitle)); + content.add(resourcePane, 0, content.getRowCount(), 2, 1); + summaryService.summarizeTo(workspace, resource, section); + } else { + // Single resource, no need to box it. + VBox wrapper = new VBox(title, subtitle); + content.add(wrapper, 0, content.getRowCount(), 2, 1); + summaryService.summarizeTo(workspace, resource, content.newSection()); + } } } @@ -107,6 +116,13 @@ private Grid() { getColumnConstraints().addAll(column1, column2); } + @Nonnull + public Grid newSection() { + Grid section = new Grid(); + add(section, 0, getRowCount(), 2, 1); + return section; + } + @Override public void appendSummary(Node node) { FxThreadUtil.run(() -> add(node, 0, getRowCount(), 2, 1)); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java index 116ea3e5c..4c2c46cc1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -20,9 +20,9 @@ import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.member.ClassMember; -import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.path.ClassMemberPathNode; import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.DirectoryPathNode; import software.coley.recaf.path.PathNode; import software.coley.recaf.services.assembler.AssemblerPipeline; import software.coley.recaf.services.assembler.AssemblerPipelineManager; @@ -50,7 +50,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; /** * Display dissassembled {@link ClassInfo} and {@link ClassMember} content. @@ -78,11 +77,11 @@ public class AssemblerPane extends AbstractContentPane> implements U @Inject public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager, - @Nonnull AssemblerToolTabs assemblerToolTabs, - @Nonnull AssemblerContextActionSupport contextActionSupport, - @Nonnull SearchBar searchBar, - @Nonnull KeybindingConfig keys, - @Nonnull Instance fieldsAndMethodsPaneProvider) { + @Nonnull AssemblerToolTabs assemblerToolTabs, + @Nonnull AssemblerContextActionSupport contextActionSupport, + @Nonnull SearchBar searchBar, + @Nonnull KeybindingConfig keys, + @Nonnull Instance fieldsAndMethodsPaneProvider) { this.pipelineManager = pipelineManager; this.assemblerToolTabs = assemblerToolTabs; this.fieldsAndMethodsPaneProvider = fieldsAndMethodsPaneProvider; @@ -301,7 +300,7 @@ private CompletableFuture> disassemble() { .orTimeout(10, TimeUnit.SECONDS) .whenCompleteAsync((result, error) -> { if (result != null) - acceptResult(result, editor::setText, ProblemPhase.LINT); + result.ifOk(editor::setText).ifErr(errors -> processErrors(errors, ProblemPhase.LINT)); else logger.error("Disassemble encountered an unexpected error", error); }, FxThreadUtil.executor()); @@ -313,7 +312,7 @@ private CompletableFuture> disassemble() { * @return Future of parse completion. */ @Nonnull - private CompletableFuture parseAST() { + private CompletableFuture>> parseAST() { // Nothing to parse if (editor.getText().isBlank()) return CompletableFuture.completedFuture(null); @@ -321,37 +320,44 @@ private CompletableFuture parseAST() { if (problemTracking.removeByPhase(ProblemPhase.LINT)) FxThreadUtil.run(editor::redrawParagraphGraphics); - return CompletableFuture.runAsync(() -> { - try { - // Tokenize the current input. - Result> tokenResult = pipeline.tokenize(editor.getText(), ""); - - // Process any errors and assign the latest token list. - if (tokenResult.hasErr()) - processErrors(tokenResult.errors(), ProblemPhase.LINT); - lastTokens = tokenResult.get(); - - // Attempt to parse the token list into 'rough' AST. - acceptResult(pipeline.roughParse(lastTokens), roughAst -> { - lastRoughAst = roughAst; - - // Attempt to complete parsing and transform the 'rough' AST into a 'concrete' AST. - acceptResult(pipeline.concreteParse(roughAst), concreteAst -> { - // The transform was a success. - lastConcreteAst = concreteAst; - eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(concreteAst, AstPhase.CONCRETE)); - }, pAst -> { - // The transform failed. - lastPartialAst = pAst; - eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(pAst, AstPhase.CONCRETE_PARTIAL)); - }, ProblemPhase.LINT); - }, pAst -> { - // We failed to parse the token list fully, but may have a partial result. - eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(pAst, AstPhase.ROUGH_PARTIAL)); - lastPartialAst = pAst; - }, ProblemPhase.LINT); - } catch (Exception ex) { - logger.error("Failed to parse assembler", ex); + return CompletableFuture.supplyAsync(() -> { + // Tokenize the current input. + Result> tokenResult = pipeline.tokenize(editor.getText(), ""); + + // Process any errors and assign the latest token list. + if (tokenResult.hasErr()) + processErrors(tokenResult.errors(), ProblemPhase.LINT); + lastTokens = tokenResult.get(); + + // Attempt to parse the token list into 'rough' AST. + Result> roughResult = pipeline.roughParse(lastTokens).ifOk(roughAst -> { + lastRoughAst = roughAst; + }).ifErr((partialAst, errors) -> { + // We failed to parse the token list fully, but may have a partial result. + eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(partialAst, AstPhase.ROUGH_PARTIAL)); + lastPartialAst = partialAst; + processErrors(errors, ProblemPhase.LINT); + }); + + // Attempt to complete parsing and transform the 'rough' AST into a 'concrete' AST. + if (roughResult.isOk()) { + return pipeline.concreteParse(roughResult.get()).ifOk(concreteAst -> { + // The transform was a success. + lastConcreteAst = concreteAst; + eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(concreteAst, AstPhase.CONCRETE)); + }).ifErr((partialAst, errors) -> { + // The transform failed. + lastPartialAst = partialAst; + eachChild(AssemblerAstConsumer.class, c -> c.consumeAst(partialAst, AstPhase.CONCRETE_PARTIAL)); + }); + } + + // Fall-back to rough AST. + return roughResult; + }).whenComplete((res, err) -> { + if (err != null) { + logger.error("Failed to parse assembler content", err); + FxThreadUtil.run(() -> Animations.animateFailure(editor, 1000)); } }); } @@ -364,8 +370,13 @@ private CompletableFuture parseAST() { @Nonnull private CompletableFuture assemble() { // Ensure the AST is up-to-date before moving onto build stage. - return parseAST().whenComplete((unused, error) -> { - if (!problemTracking.getProblems().isEmpty() || lastConcreteAst == null) + return parseAST().thenAccept(astResult -> { + // If the parse finished, clear old build errors. + if (problemTracking.removeByPhase(ProblemPhase.BUILD)) + FxThreadUtil.run(editor::redrawParagraphGraphics); + + // Skip if any problems remain in the AST + if (!problemTracking.getProblemsByPhase(ProblemPhase.LINT).isEmpty() || lastConcreteAst == null) return; // Clear build errors since we are running the build process again. @@ -379,19 +390,62 @@ private CompletableFuture assemble() { lastAssembledClassRepresentation = representation; if (representation instanceof JavaClassRepresentation javaClassRep) { - lastAssembledClass = pipeline.getClassInfo(Unchecked.cast(javaClassRep)); + // Get last class's field/method count for ensuring no duplication happened. + // If the user changes the definition of a field/method it inserts a new one + // instead of changing the existing declaration. + int fieldCount = lastAssembledClass.getFields().size(); + int methodCount = lastAssembledClass.getMethods().size(); + + // Update last class. + ClassInfo assembledClass = pipeline.getClassInfo(Unchecked.cast(javaClassRep)); // Update the local path value, this will also inform sub-components of the new content. // The update-lock must be set so that we don't trigger a disassembly (which would trigger an endless loop) updateLock.set(true); if (path instanceof ClassPathNode classPath) { - ClassPathNode newPath = classPath.getParent().child(lastAssembledClass); - onUpdatePath(newPath); + // Only update if the class name was untouched. + if (assembledClass.getName().equals(classPath.getValue().getName())) { + ClassPathNode newPath = classPath.getParent().child(assembledClass); + onUpdatePath(newPath); + lastAssembledClass = assembledClass; + } else { + ASTElement sourceAst = lastConcreteAst.get(0); + Error err = new Error("Changing the class name is not allowed in the assembler.\n" + + "Use Recaf's refactoring capabilities to rename classes.", sourceAst.location()); + processErrors(List.of(err), ProblemPhase.BUILD); + } } else if (path instanceof ClassMemberPathNode memberPath) { ClassMember oldMember = memberPath.getValue(); - MethodMember newMember = lastAssembledClass.getDeclaredMethod(oldMember.getName(), oldMember.getDescriptor()); - ClassMemberPathNode newPath = memberPath.getParent().getParent().child(lastAssembledClass).child(newMember); - onUpdatePath(newPath); + ClassMember newMember; + String memberType; + if (oldMember.isMethod()) { + memberType = "method"; + if (assembledClass.getMethods().size() == methodCount) { + newMember = assembledClass.getDeclaredMethod(oldMember.getName(), oldMember.getDescriptor()); + } else { + newMember = null; + } + } else { + memberType = "field"; + if (assembledClass.getFields().size() == fieldCount) { + newMember = assembledClass.getDeclaredField(oldMember.getName(), oldMember.getDescriptor()); + } else { + newMember = null; + } + } + + // Only update if the member name/type was untouched. + if (newMember != null) { + lastAssembledClass = assembledClass; + DirectoryPathNode packagePath = memberPath.getParent().getParent(); + ClassMemberPathNode newPath = packagePath.child(lastAssembledClass).child(newMember); + onUpdatePath(newPath); + } else { + ASTElement sourceAst = lastConcreteAst.get(0); + Error err = new Error("Changing the " + memberType + " name/type is not allowed in the assembler.\n" + + "Use Recaf's refactoring capabilities to rename fields & methods.", sourceAst.location()); + processErrors(List.of(err), ProblemPhase.BUILD); + } } updateLock.set(false); } @@ -461,15 +515,4 @@ private void processErrors(@Nonnull Collection errors, @Nonnull ProblemPh editor.redrawParagraphGraphics(); }); } - - private void acceptResult(Result result, Consumer acceptor, ProblemPhase phase) { - result.ifOk(acceptor).ifErr(errors -> processErrors(errors, phase)); - } - - private void acceptResult(Result result, Consumer acceptor, Consumer pAcceptor, ProblemPhase phase) { - result.ifOk(acceptor).ifErr((pOk, errors) -> { - pAcceptor.accept(pOk); - processErrors(errors, phase); - }); - } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/InheritancePane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/InheritancePane.java index 1ddc26451..bd77354b3 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/InheritancePane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/InheritancePane.java @@ -65,7 +65,6 @@ public InheritancePane(@Nonnull InheritanceGraph inheritanceGraph, Lang.get("hierarchy.children") : Lang.get("hierarchy.parents"))); toggle.getStyleClass().add(Styles.ROUNDED); toggle.setFocusTraversable(false); - toggle.setPrefWidth(100); StackPane.setAlignment(toggle, Pos.BOTTOM_RIGHT); StackPane.setMargin(toggle, new Insets(10)); diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index db94db6e4..4dc529598 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -723,6 +723,8 @@ service.ui.class-editing-config.default-jvm-editor=Default editor for JVM classe service.ui.decompile-pane-config=Decompilation panel service.ui.decompile-pane-config.timeout-seconds=Decompiler timeout (seconds) service.ui.decompile-pane-config.mapping-acceleration=Accelerate remapping operations +service.ui.member-format-config=Field & method format +service.ui.member-format-config.name-type-display=Name & type display service.ui.text-format-config=Text format service.ui.text-format-config.escape=Enable text escapes service.ui.text-format-config.max-length=Maximum text display length