Skip to content

Commit

Permalink
Merge branch 'pr/907'
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Jan 16, 2025
2 parents f39765f + 1aafad1 commit 151f28e
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 30 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ image-io-ext-ico = "3.0.2"
instrument-server = "1.4.2"
jackson = "2.17.1"
jakarta-annotation = "3.0.0"
jasm = "1f1424fcba"
jasm = "9aaf9538a8"
jlinker = "1.0.7"
jphantom = "1.4.4"
junit = "5.11.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
* @author Matt Coley
*/
public class AssemblerTabCompleter implements TabCompleter<AssemblerTabCompleter.AssemblerCompletion> {
private final CompletionPopup<AssemblerCompletion> completionPopup = new AssemblerCompletionPopup(15);
private final CompletionPopup<AssemblerCompletion> completionPopup;
private final Workspace workspace;
private final InheritanceGraph inheritanceGraph;
private final CellConfigurationService configurationService;
private final TabCompletionConfig config;
private CodeArea area;
private List<ASTElement> ast;
private Context context;
Expand All @@ -56,13 +57,20 @@ public class AssemblerTabCompleter implements TabCompleter<AssemblerTabCompleter
* Workspace to pull class info from.
* @param inheritanceGraph
* Graph to pull hierarchies from.
* @param configurationService
* Service to configure cell content.
* @param config
* Tab completion config.
*/
public AssemblerTabCompleter(@Nonnull Workspace workspace,
@Nonnull InheritanceGraph inheritanceGraph,
@Nonnull CellConfigurationService configurationService) {
@Nonnull CellConfigurationService configurationService,
@Nonnull TabCompletionConfig config) {
this.workspace = workspace;
this.inheritanceGraph = inheritanceGraph;
this.configurationService = configurationService;
this.completionPopup = new AssemblerCompletionPopup(config);
this.config = config;
}

/**
Expand Down Expand Up @@ -96,7 +104,9 @@ public boolean requestCompletion(@Nonnull KeyEvent event) {
@Nonnull
@Override
public List<AssemblerCompletion> computeCurrentCompletions() {
return context.complete();
return context.complete().stream()
.filter(completion -> completion.text().length() <= config.getMaxCompletionLength())
.toList();
}

@Override
Expand Down Expand Up @@ -139,17 +149,15 @@ private void recomputeLineContext() {

// Must be in code block
boolean inCode = false;
ASTElement element = ast.getFirst();
int caret = area.getCaretPosition();
for (int i = 0; i < 5; i++) {
if (element instanceof ASTMethod method) {
ASTCode code = method.code();
ASTElement element = ast.getFirst().pick(caret);
while (element != null){
if (element instanceof ASTCode code) {
if (code.range().within(caret))
inCode = true;
break;
} else {
element = element.pick(caret);
}
element = element.parent();
}
if (!inCode) {
context = new EmptyContext();
Expand Down Expand Up @@ -225,8 +233,8 @@ private static boolean completeFromContext(@Nullable String context, @Nonnull Pr
}

private class AssemblerCompletionPopup extends CompletionPopup<AssemblerCompletion> {
private AssemblerCompletionPopup(int maxItemsToShow) {
super(STANDARD_CELL_SIZE, maxItemsToShow, AssemblerCompletion::text, completion -> switch (completion) {
private AssemblerCompletionPopup(@Nonnull TabCompletionConfig config) {
super(config, STANDARD_CELL_SIZE, AssemblerCompletion::text, completion -> switch (completion) {
case AssemblerCompletion.Opcode opcode -> {
String text = opcode.text();
if (text.startsWith("invoke"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package software.coley.recaf.ui.control.richtext.suggest;

import com.sun.javafx.util.Utils;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.IndexRange;
import javafx.scene.control.ListCell;
Expand All @@ -13,6 +15,7 @@
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Popup;
import javafx.stage.Screen;
import org.fxmisc.richtext.CodeArea;
import software.coley.recaf.ui.control.richtext.Editor;

Expand All @@ -36,7 +39,7 @@ public abstract class CompletionPopup<T> {
private final ListView<T> listView = new ListView<>();
private final ScrollPane scrollPane = new ScrollPane(listView);
private final CompletionValueTextifier<T> textifier;
private final int maxItemsToShow;
private final TabCompletionConfig config;
private final int cellSize;
private CompletionPopupFocuser completionPopupFocuser;
private CompletionPopupUpdater<T> completionPopupUpdater;
Expand All @@ -46,31 +49,32 @@ public abstract class CompletionPopup<T> {
private T selected;

/**
* @param config
* Tab completion config.
* @param cellSize
* Height of cells in the list-view of completion items.
* @param maxItemsToShow
* Number of cells to show at a time.
* @param textifier
* Mapper of {@code T} values to {@code String}.
*/
public CompletionPopup(int cellSize, int maxItemsToShow, @Nonnull CompletionValueTextifier<T> textifier) {
this(cellSize, maxItemsToShow, textifier, t -> null);
public CompletionPopup(@Nonnull TabCompletionConfig config, int cellSize,
@Nonnull CompletionValueTextifier<T> textifier) {
this(config, cellSize, textifier, t -> null);
}

/**
* @param config
* Tab completion config.
* @param cellSize
* Height of cells in the list-view of completion items.
* @param maxItemsToShow
* Number of cells to show at a time.
* @param textifier
* Mapper of {@code T} values to {@code String}.
* @param graphifier
* Mapper of {@code T} values to display graphics.
*/
public CompletionPopup(int cellSize, int maxItemsToShow,
public CompletionPopup(@Nonnull TabCompletionConfig config, int cellSize,
@Nonnull CompletionValueTextifier<T> textifier,
@Nonnull CompletionValueGraphicMapper<T> graphifier) {
this.maxItemsToShow = maxItemsToShow;
this.config = config;
this.textifier = textifier;

// Ensure scroll-pane is 'fit-to-height' so there's no empty space wasting virtual scroll space.
Expand All @@ -82,6 +86,10 @@ public CompletionPopup(int cellSize, int maxItemsToShow,
// and isn't overly aggressive to hide it too often.
popup.setAutoHide(true);

// Auto-fix can move the popup infront of the caret, which is not what we want.
// In #show() we will manually adjust the position of the popup to ensure it is on screen.
popup.setAutoFix(false);

// For simplicity of a few operations we're going to ensure all cells are a fixed size.
listView.setFixedCellSize(this.cellSize = cellSize);
listView.setPrefWidth(400);
Expand Down Expand Up @@ -227,9 +235,40 @@ public void hide() {
* @see Popup#show(Node, double, double)
*/
public void show() {
// If the popup has content to show, show it above where the caret position is.
if (popupSize > 0)
popup.show(area, lastCaretBounds.getMinX(), lastCaretBounds.getMinY() - popupSize);
// If the popup has content to show
if (popupSize <= 0) {
return;
}
double anchorX = config.getPopupPosition().isRight()
? lastCaretBounds.getMaxX()
: lastCaretBounds.getMinX() - popup.getWidth();
double anchorY = config.getPopupPosition().isAbove()
? lastCaretBounds.getMaxY()
: lastCaretBounds.getMinY() - popupSize;

// choose other position if the popup is off-screen
// if the popup is off-screen, flip the popup to the other side of the caret on that axis
// (it will also avoid popup from being split between two screens)

// code loosely adapted from PopupWindow#updateWindow(double, double)
final Screen currentScreen = Utils.getScreenForPoint(anchorX, anchorY);
final Rectangle2D screenBounds =
Utils.hasFullScreenStage(currentScreen)
? currentScreen.getBounds()
: currentScreen.getVisualBounds();

if (anchorY + popupSize > screenBounds.getMaxY()) {
anchorY = lastCaretBounds.getMinY() - popupSize;
} else if (anchorY < screenBounds.getMinY()) {
anchorY = lastCaretBounds.getMaxY();
}
if (anchorX + popup.getWidth() > screenBounds.getMaxX()) {
anchorX = lastCaretBounds.getMinX() - popup.getWidth();
} else if (anchorX < screenBounds.getMinX()) {
anchorX = lastCaretBounds.getMaxX();
}

popup.show(area, anchorX, anchorY);
}

/**
Expand Down Expand Up @@ -285,7 +324,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, config.getMaxCompletionRows()) + 1);
scrollPane.setPrefHeight(popupSize);
scrollPane.setMaxHeight(popupSize);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@
*/
public class ExistingWordTabCompleter implements TabCompleter<String> {
private final Set<String> words = ConcurrentHashMap.newKeySet();
private final CompletionPopup<String> completionPopup = new StringCompletionPopup(8);
private final CompletionPopup<String> completionPopup;
private CodeArea area;
private String lineContext;

/**
* @param config
* Tab completion config.
*/
public ExistingWordTabCompleter(@Nonnull TabCompletionConfig config) {
completionPopup = new StringCompletionPopup(config);
}

@Override
public boolean requestCompletion(@Nonnull KeyEvent event) {
// Recompute line context to ensure its up-to-date.
Expand Down Expand Up @@ -132,8 +140,8 @@ private static String computeCompletionContext(@Nonnull String line, int column)
}

private class StringCompletionPopup extends CompletionPopup<String> {
private StringCompletionPopup(int maxItemsToShow) {
super(STANDARD_CELL_SIZE, maxItemsToShow, t -> t);
private StringCompletionPopup(@Nonnull TabCompletionConfig config) {
super(config, STANDARD_CELL_SIZE, t -> t);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package software.coley.recaf.ui.control.richtext.suggest;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.observables.ObservableBoolean;
import software.coley.observables.ObservableInteger;
import software.coley.observables.ObservableObject;
import software.coley.recaf.config.BasicConfigContainer;
import software.coley.recaf.config.BasicConfigValue;
import software.coley.recaf.config.ConfigGroups;
Expand All @@ -15,12 +18,26 @@
*/
@ApplicationScoped
public class TabCompletionConfig extends BasicConfigContainer {
private final ObservableObject<PopupPosition> popupPosition = new ObservableObject<>(PopupPosition.ABOVE_RIGHT);
private final ObservableBoolean enabledInAssembler = new ObservableBoolean(true);
private final ObservableInteger maxCompletionRows = new ObservableInteger(15);
private final ObservableInteger maxCompletionLength = new ObservableInteger(200);

@Inject
public TabCompletionConfig() {
super(ConfigGroups.SERVICE_UI, "tab-completion" + CONFIG_SUFFIX);
addValue(new BasicConfigValue<>("popup-position", PopupPosition.class, popupPosition));
addValue(new BasicConfigValue<>("enabled-in-assembler", boolean.class, enabledInAssembler));
addValue(new BasicConfigValue<>("max-completion-rows", int.class, maxCompletionRows));
addValue(new BasicConfigValue<>("max-completion-length", int.class, maxCompletionLength));
}

/**
* @return Current popup position.
*/
@Nonnull
public PopupPosition getPopupPosition() {
return popupPosition.getValue();
}

/**
Expand All @@ -29,4 +46,45 @@ public TabCompletionConfig() {
public boolean isEnabledInAssembler() {
return enabledInAssembler.getValue();
}

/**
* @return Number of completions to visually show in a popup/overlay. Always {@code >= 1}.
*/
public int getMaxCompletionRows() {
return Math.max(1, maxCompletionRows.getValue());
}

/**
* @return Max length of a completion string to allow.
*/
public int getMaxCompletionLength() {
return maxCompletionLength.getValue();
}

public enum PopupPosition {
/**
* Popup appears above and to right of the cursor
*/
ABOVE_RIGHT,
/**
* Popup appears below and to right of the cursor
*/
BELOW_RIGHT,
/**
* Popup appears above and to left of the cursor
*/
ABOVE_LEFT,
/**
* Popup appears below and to left of the cursor
*/
BELOW_LEFT;

public boolean isAbove() {
return this == ABOVE_RIGHT || this == ABOVE_LEFT;
}

public boolean isRight() {
return this == ABOVE_RIGHT || this == BELOW_RIGHT;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager,
InheritanceGraph inheritanceGraph = Objects.requireNonNull(graphService.getCurrentWorkspaceInheritanceGraph(), "Graph not created");
if (tabCompletionConfig.isEnabledInAssembler()) {
Workspace workspace = Objects.requireNonNull(workspaceManager.getCurrent());
tabCompleter = new AssemblerTabCompleter(workspace, inheritanceGraph, cellConfigurationService);
tabCompleter = new AssemblerTabCompleter(workspace, inheritanceGraph, cellConfigurationService, tabCompletionConfig);
editor.setTabCompleter(tabCompleter);
}
editor.getCodeArea().getStylesheets().add(LanguageStylesheets.getJasmStylesheet());
Expand Down
5 changes: 4 additions & 1 deletion recaf-ui/src/main/resources/translations/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,9 @@ service.ui.member-format-config.name-type-display=Name & type display
service.ui.tab-completion-config=Tab Completion
service.ui.text-format-config=Text format
service.ui.tab-completion-config.enabled-in-assembler=Enabled in assembler
service.ui.tab-completion-config.max-completion-length=Max completion length
service.ui.tab-completion-config.max-completion-rows=Displayed completion rows
service.ui.tab-completion-config.popup-position=Preferred completion popup position in relation to cursor
service.ui.text-format-config.escape=Enable text escapes
service.ui.text-format-config.max-length=Maximum text display length
service.ui.text-format-config.shorten=Enable text shortening
Expand Down Expand Up @@ -856,4 +859,4 @@ misc.position.bottom=Bottom
misc.position.left=Left
misc.position.right=Right
misc.position.center=Center
misc.position.middle=Middle
misc.position.middle=Middle

0 comments on commit 151f28e

Please sign in to comment.