Skip to content

Commit

Permalink
Enhanced member tab completion in the assembler
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Sep 15, 2024
1 parent 12e2602 commit 6ce4a6f
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,12 @@ private InheritanceStubVertex() {
}

@Override
public boolean hasField(String name, String desc) {
public boolean hasField(@Nonnull String name, @Nonnull String desc) {
return false;
}

@Override
public boolean hasMethod(String name, String desc) {
public boolean hasMethod(@Nonnull String name, @Nonnull String desc) {
return false;
}

Expand All @@ -485,22 +485,22 @@ public boolean isJavaLangObject() {
}

@Override
public boolean isParentOf(InheritanceVertex vertex) {
public boolean isParentOf(@Nonnull InheritanceVertex vertex) {
return false;
}

@Override
public boolean isChildOf(InheritanceVertex vertex) {
public boolean isChildOf(@Nonnull InheritanceVertex vertex) {
return false;
}

@Override
public boolean isIndirectFamilyMember(InheritanceVertex vertex) {
public boolean isIndirectFamilyMember(@Nonnull InheritanceVertex vertex) {
return false;
}

@Override
public boolean isIndirectFamilyMember(Set<InheritanceVertex> family, InheritanceVertex vertex) {
public boolean isIndirectFamilyMember(@Nonnull Set<InheritanceVertex> family, @Nonnull InheritanceVertex vertex) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public class InheritanceVertex {
* Flag for if the class belongs to a workspaces primary resource.
*/
public InheritanceVertex(@Nonnull ClassInfo value,
@Nonnull Function<String, InheritanceVertex> lookup,
@Nonnull Function<String, Collection<String>> childrenLookup, boolean isPrimary) {
@Nonnull Function<String, InheritanceVertex> lookup,
@Nonnull Function<String, Collection<String>> childrenLookup, boolean isPrimary) {
this.value = value;
this.lookup = lookup;
this.childrenLookup = childrenLookup;
Expand All @@ -54,7 +54,7 @@ public InheritanceVertex(@Nonnull ClassInfo value,
*
* @return If the field exists in the current vertex.
*/
public boolean hasField(String name, String desc) {
public boolean hasField(@Nonnull String name, @Nonnull String desc) {
for (FieldMember fn : value.getFields())
if (fn.getName().equals(name) && fn.getDescriptor().equals(desc))
return true;
Expand All @@ -69,7 +69,7 @@ public boolean hasField(String name, String desc) {
*
* @return If the field exists in the current vertex or in any parent vertex.
*/
public boolean hasFieldInSelfOrParents(String name, String desc) {
public boolean hasFieldInSelfOrParents(@Nonnull String name, @Nonnull String desc) {
if (hasField(name, desc))
return true;
return allParents()
Expand All @@ -85,7 +85,7 @@ public boolean hasFieldInSelfOrParents(String name, String desc) {
*
* @return If the field exists in the current vertex or in any child vertex.
*/
public boolean hasFieldInSelfOrChildren(String name, String desc) {
public boolean hasFieldInSelfOrChildren(@Nonnull String name, @Nonnull String desc) {
if (hasField(name, desc))
return true;
return allChildren()
Expand All @@ -101,7 +101,7 @@ public boolean hasFieldInSelfOrChildren(String name, String desc) {
*
* @return If the method exists in the current vertex.
*/
public boolean hasMethod(String name, String desc) {
public boolean hasMethod(@Nonnull String name, @Nonnull String desc) {
for (MethodMember mn : value.getMethods())
if (mn.getName().equals(name) && mn.getDescriptor().equals(desc))
return true;
Expand All @@ -116,7 +116,7 @@ public boolean hasMethod(String name, String desc) {
*
* @return If the method exists in the current vertex or in any parent vertex.
*/
public boolean hasMethodInSelfOrParents(String name, String desc) {
public boolean hasMethodInSelfOrParents(@Nonnull String name, @Nonnull String desc) {
if (hasMethod(name, desc))
return true;
return allParents()
Expand All @@ -132,7 +132,7 @@ public boolean hasMethodInSelfOrParents(String name, String desc) {
*
* @return If the method exists in the current vertex or in any child vertex.
*/
public boolean hasMethodInSelfOrChildren(String name, String desc) {
public boolean hasMethodInSelfOrChildren(@Nonnull String name, @Nonnull String desc) {
if (hasMethod(name, desc))
return true;
return allChildren()
Expand Down Expand Up @@ -181,7 +181,7 @@ public boolean isModule() {
* @return {@code true} if method is an extension of an outside class's methods and thus should not be renamed.
* {@code false} if the method is safe to rename.
*/
public boolean isLibraryMethod(String name, String desc) {
public boolean isLibraryMethod(@Nonnull String name, @Nonnull String desc) {
// Check against this definition
if (!isPrimary && hasMethod(name, desc))
return true;
Expand All @@ -202,7 +202,7 @@ public boolean isLibraryMethod(String name, String desc) {
*
* @return {@code true} if the vertex is of a child type to this vertex's {@link #getName() type}.
*/
public boolean isParentOf(InheritanceVertex vertex) {
public boolean isParentOf(@Nonnull InheritanceVertex vertex) {
return vertex.getAllParents().contains(this);
}

Expand All @@ -212,7 +212,7 @@ public boolean isParentOf(InheritanceVertex vertex) {
*
* @return {@code true} if the vertex is of a parent type to this vertex's {@link #getName() type}.
*/
public boolean isChildOf(InheritanceVertex vertex) {
public boolean isChildOf(@Nonnull InheritanceVertex vertex) {
return getAllParents().contains(vertex);
}

Expand All @@ -222,7 +222,7 @@ public boolean isChildOf(InheritanceVertex vertex) {
*
* @return {@code true} if the vertex is a family member, but is not a child or parent of the current vertex.
*/
public boolean isIndirectFamilyMember(InheritanceVertex vertex) {
public boolean isIndirectFamilyMember(@Nonnull InheritanceVertex vertex) {
return isIndirectFamilyMember(getFamily(true), vertex);
}

Expand All @@ -234,7 +234,7 @@ public boolean isIndirectFamilyMember(InheritanceVertex vertex) {
*
* @return {@code true} if the vertex is a family member, but is not a child or parent of the current vertex.
*/
public boolean isIndirectFamilyMember(Set<InheritanceVertex> family, InheritanceVertex vertex) {
public boolean isIndirectFamilyMember(@Nonnull Set<InheritanceVertex> family, @Nonnull InheritanceVertex vertex) {
return this != vertex &&
family.contains(vertex) &&
!isChildOf(vertex) &&
Expand Down Expand Up @@ -284,7 +284,7 @@ public Set<InheritanceVertex> getFamily(boolean includeObject) {
return vertices;
}

private void visitFamily(Set<InheritanceVertex> vertices) {
private void visitFamily(@Nonnull Set<InheritanceVertex> vertices) {
if (isModule())
return;
if (vertices.add(this) && !isJavaLangObject())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.model.PlainTextChange;
import regexodus.Matcher;
import software.coley.collections.tree.Tree;
import software.coley.collections.Unchecked;
import software.coley.recaf.info.ClassInfo;
import software.coley.recaf.info.member.ClassMember;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.services.inheritance.InheritanceGraph;
import software.coley.recaf.services.inheritance.InheritanceVertex;
import software.coley.recaf.ui.control.richtext.Editor;
import software.coley.recaf.ui.pane.editing.assembler.AssemblerPane;
import software.coley.recaf.util.ClasspathUtil;
Expand All @@ -23,7 +25,6 @@
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -34,16 +35,20 @@
public class AssemblerTabCompleter implements TabCompleter<String> {
private final CompletionPopup<String> completionPopup = new StringCompletionPopup(15);
private final Workspace workspace;
private final InheritanceGraph graph;
private CodeArea area;
private List<ASTElement> ast;
private Context context;

/**
* @param workspace
* Workspace to pull class info from.
* @param graph
* Graph to pull hierarchies from.
*/
public AssemblerTabCompleter(@Nonnull Workspace workspace) {
public AssemblerTabCompleter(@Nonnull Workspace workspace, @Nonnull InheritanceGraph graph) {
this.workspace = workspace;
this.graph = graph;
}

/**
Expand Down Expand Up @@ -129,7 +134,12 @@ private void recomputeLineContext() {
}

// Get the line to regex match, and the trimmed slice of it for tab-completion's "partial text" to complete.
// The line content we track should be up to the caret, but no further since we're completing the text where
// the caret is at and not the end of the line.
int caretColumn = area.getCaretColumn();
String line = area.getParagraph(area.getCurrentParagraph()).getText();
if (caretColumn <= line.length())
line = line.substring(0, caretColumn);
String partialText = line.trim();

// Skip if there is nothing to complete.
Expand Down Expand Up @@ -190,6 +200,9 @@ private static boolean completeFromContext(@Nullable String context, @Nonnull Pr
}

private class StringCompletionPopup extends CompletionPopup<String> {
// TODO: For better richness in the UI, we should migrate away from just 'String' and have a model
// that includes information about the completion (be it opcodes, fields, methods, types, etc)

private StringCompletionPopup(int maxItemsToShow) {
super(STANDARD_CELL_SIZE, maxItemsToShow, t -> t);
}
Expand Down Expand Up @@ -273,7 +286,7 @@ public CodeFieldContext(@Nonnull String partialInput, @Nullable String owner, @N
@Override
public List<String> complete() {
boolean isStatic = partialInput.contains("getstatic ") || partialInput.contains("putstatic ");
return complete(workspace, c -> c.fieldStream().filter(m -> isStatic == m.hasStaticModifier()));
return complete(workspace, graph, c -> c.fieldStream().filter(m -> isStatic == m.hasStaticModifier()));
}
}

Expand All @@ -300,10 +313,16 @@ class CodeMethodContext extends ReferenceContext {
@Nonnull
@Override
public List<String> complete() {
// TODO: Will probably want to support more than just the declared methods
// - If a class "Foo" extends "Bar" any "Foo.x" should complete available methods from "Bar"
boolean isStatic = partialInput.contains("invokestatic ") || partialInput.contains("invokestaticinterface ");
return complete(workspace, c -> c.methodStream().filter(m -> isStatic == m.hasStaticModifier()));
boolean isSpecial = partialInput.contains("invokespecial ");
return complete(workspace, graph, c -> c.methodStream().filter(m -> {
// Only invokespecial can be used to call constructors.
if (m.getName().startsWith("<") && !isSpecial)
return false;

// Limit by static matching
return isStatic == m.hasStaticModifier();
}));
}
}

Expand Down Expand Up @@ -341,47 +360,82 @@ public final String partialMatchedText() {
public abstract List<String> complete();

@Nonnull
protected List<String> complete(@Nonnull Workspace workspace,
protected List<String> complete(@Nonnull Workspace workspace, @Nonnull InheritanceGraph graph,
@Nonnull Function<ClassInfo, Stream<? extends ClassMember>> classMemberLookup) {
// Skip if no owner type specified.
if (owner == null) return Collections.emptyList();

// Complete class names
// Complete class names if the '.' separator is not seen, otherwise suggest any member in the class.
if (name == null && desc == null) {
// TODO: startsWith is nice, but mirroring IntelliJ would be nicer
// - Should be able to complete "new Strin" to "new java/lang/String"
// - But out completion system just appends text so we'd need to do some more work there.

Stream<String> a = ClasspathUtil.getSystemClassSet().stream()
.filter(s -> s.startsWith(owner));
Stream<String> b = workspace.findClasses(c -> c.getName().startsWith(owner)).stream()
.map(c -> c.getValue().getName());
return Stream.concat(a, b).toList();
// - Also like IntelliJ we should prioritize common completions over niche ones
// - Also not something that happens right here
if (partialInput.endsWith(".")) {
ClassPathNode ownerPath = workspace.findClass(owner);
if (ownerPath != null) {
Set<String> items = new TreeSet<>();
InheritanceVertex vertex = graph.getVertex(owner);
if (vertex != null)
for (InheritanceVertex parent : vertex.getAllParents())
items.addAll(collect(classMemberLookup.apply(parent.getValue())));
items.addAll(collect(classMemberLookup.apply(ownerPath.getValue())));
return items.isEmpty() ? Collections.emptyList() : new ArrayList<>(items);
}
} else {
// No separator, user is still typing out a class name.
Stream<String> a = ClasspathUtil.getSystemClassSet().stream()
.filter(s -> s.startsWith(owner));
Stream<String> b = workspace.findClasses(c -> c.getName().startsWith(owner)).stream()
.map(c -> c.getValue().getName());
return Stream.concat(a, b).toList();
}
}

// Complete member name/descriptors if we have "owner.x"
if (desc == null) {
if (name != null && desc == null) {
ClassPathNode ownerPath = workspace.findClass(owner);
if (ownerPath != null) {
return classMemberLookup.apply(ownerPath.getValue())
.filter(member -> member.getName().startsWith(name))
.map(member -> member.getName() + " " + member.getDescriptor())
.toList();
Set<String> items = new TreeSet<>();
InheritanceVertex vertex = graph.getVertex(owner);
Predicate<ClassMember> filter = member -> member.getName().startsWith(name);
if (vertex != null)
for (InheritanceVertex parent : vertex.getAllParents())
items.addAll(collect(classMemberLookup.apply(parent.getValue()), filter));
items.addAll(collect(classMemberLookup.apply(ownerPath.getValue()), filter));
return items.isEmpty() ? Collections.emptyList() : new ArrayList<>(items);
}
}

// Complete member descriptors if we have "owner.name"
if (desc != null) {
ClassPathNode ownerPath = workspace.findClass(owner);
if (ownerPath != null) {
return classMemberLookup.apply(ownerPath.getValue())
.filter(member -> member.getName().equals(name) && member.getDescriptor().startsWith(desc))
.map(ClassMember::getDescriptor)
.toList();
Set<String> items = new TreeSet<>();
InheritanceVertex vertex = graph.getVertex(owner);
Predicate<ClassMember> filter = member -> member.getName().equals(name) && member.getDescriptor().startsWith(desc);
if (vertex != null)
for (InheritanceVertex parent : vertex.getAllParents())
items.addAll(collect(classMemberLookup.apply(parent.getValue()), filter));
items.addAll(collect(classMemberLookup.apply(ownerPath.getValue()), filter));
return items.isEmpty() ? Collections.emptyList() : new ArrayList<>(items);
}
}

return Collections.emptyList();
}

@Nonnull
private static List<String> collect(@Nonnull Stream<? extends ClassMember> stream) {
return collect(stream, null);
}

@Nonnull
private static List<String> collect(@Nonnull Stream<? extends ClassMember> stream, @Nullable Predicate<? extends ClassMember> predicate) {
if (predicate != null)
stream = stream.filter(Unchecked.cast(predicate));
return stream.map(member -> member.getName() + " " + member.getDescriptor()).toList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ public boolean updateCaretBoundsIndirect() {
return false;
}

/**
* @return {@code true} when the associated text area has selected text.
*/
public boolean hasTextSelection() {
return area.getSelection().getLength() > 0;
}

/**
* Updates the items to support completion of, and updates the size of the UI
* to fit exactly the number of items to show.
Expand All @@ -246,7 +253,7 @@ public void updateItems(@Nonnull List<T> items) {
// - Needs a bit of padding due to the way borders/scrollbars render
// The scollpane should be dictating the size since it is the popup content root.
int itemCount = items.size();
popupSize = cellSize * (Math.min(itemCount, maxItemsToShow + 1));
popupSize = cellSize * (Math.min(itemCount, maxItemsToShow) + 1);
scrollPane.setPrefHeight(popupSize);
scrollPane.setMaxHeight(popupSize);

Expand Down
Loading

0 comments on commit 6ce4a6f

Please sign in to comment.