Bind editor style layers to properties

This commit is contained in:
Clément Fournier
2019-01-29 18:13:49 +01:00
parent b83d06028e
commit 4829404a67
5 changed files with 99 additions and 147 deletions

View File

@ -17,11 +17,11 @@ import java.util.Stack;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.reactfx.Subscription;
import org.reactfx.value.Val;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.util.fxdesigner.app.AbstractController;
import net.sourceforge.pmd.util.fxdesigner.app.CompositeSelectionSource;
import net.sourceforge.pmd.util.fxdesigner.app.DesignerRoot;
@ -133,7 +133,11 @@ public class MainDesignerController extends AbstractController<AbstractControlle
setupAuxclasspathMenuItem.setOnAction(e -> sourceEditorController.showAuxclasspathSetupPopup());
openEventLogMenuItem.setOnAction(e -> eventLogController.getValue().showPopup());
openEventLogMenuItem.setOnAction(e -> {
EventLogController wizard = eventLogController.getValue();
Subscription parentToWizSubscription = wizard.errorNodesProperty().values().subscribe(sourceEditorController.currentErrorNodesProperty()::setValue);
wizard.showPopup(parentToWizSubscription);
});
openEventLogMenuItem.textProperty().bind(
getLogger().numNewLogEntriesProperty().map(i -> "Event log (" + (i > 0 ? i : "no") + " new)")
);
@ -145,10 +149,12 @@ public class MainDesignerController extends AbstractController<AbstractControlle
protected void afterChildrenInit() {
updateRecentFilesMenu();
refreshAST(); // initial refreshing
sourceEditorController.moveCaret(0, 0);
sourceEditorController.currentRuleResultsProperty().bind(xpathPanelController.currentResultsProperty());
// this is the only place where getSelectionEvents is called
getSelectionEvents().distinct().subscribe(n -> CompositeSelectionSource.super.bubbleDown(n));
}
@ -199,15 +205,6 @@ public class MainDesignerController extends AbstractController<AbstractControlle
}
/**
* Highlight a list of name occurrences.
*
* @param occurrences May be empty but never null.
*/
public void highlightAsNameOccurences(List<NameOccurrence> occurrences) {
sourceEditorController.highlightNameOccurrences(occurrences);
}
/**
* Runs an XPath (2.0) query on the current AST.
* Performs no side effects.
@ -223,31 +220,6 @@ public class MainDesignerController extends AbstractController<AbstractControlle
}
/**
* Handles nodes that potentially caused an error.
* This can for example highlight nodes on the
* editor. Effects can be reset with {@link #resetSelectedErrorNodes()}.
*
* @param n Node
*/
public void handleSelectedNodeInError(List<Node> n) {
resetSelectedErrorNodes();
sourceEditorController.highlightErrorNodes(n);
}
public void resetSelectedErrorNodes() {
sourceEditorController.clearErrorNodes();
}
public void resetXPathResults() {
sourceEditorController.clearXPathHighlight();
}
/** Replaces previously highlighted XPath results with the given nodes. */
public void highlightXPathResults(List<Node> nodes) {
sourceEditorController.highlightXPathResults(nodes);
}
private void showLicensePopup() {
Alert licenseAlert = new Alert(AlertType.INFORMATION);
licenseAlert.setWidth(500);

View File

@ -8,13 +8,9 @@ import static net.sourceforge.pmd.util.fxdesigner.util.DesignerIteratorUtil.pare
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.reactfx.EventStream;
@ -27,9 +23,6 @@ import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.ast.xpath.Attribute;
import net.sourceforge.pmd.lang.metrics.LanguageMetricsProvider;
import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;
import net.sourceforge.pmd.lang.symboltable.ScopedNode;
import net.sourceforge.pmd.util.fxdesigner.app.AbstractController;
import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource;
import net.sourceforge.pmd.util.fxdesigner.model.MetricResult;
@ -122,13 +115,6 @@ public class NodeInfoPanelController extends AbstractController<MainDesignerCont
}
@Override
public boolean alwaysHandleSelection() {
// it needs to handle name occurrences
return true;
}
/**
* Displays info about a node. If null, the panels are reset.
*
@ -149,49 +135,6 @@ public class NodeInfoPanelController extends AbstractController<MainDesignerCont
Platform.runLater(() -> displayAttributes(node));
Platform.runLater(() -> displayMetrics(node));
displayScopes(node);
if (node instanceof ScopedNode) {
// not null as well
highlightNameOccurences((ScopedNode) node);
}
}
private void highlightNameOccurences(ScopedNode node) {
// For MethodNameDeclaration the scope is the method scope, which is not the scope it is declared
// in but the scope it declares! That means that getDeclarations().get(declaration) returns null
// and no name occurrences are found. We thus look in the parent, but ultimately the name occurrence
// finder is broken since it can't find e.g. the use of a method in another scope. Plus in case of
// overloads both overloads are reported to have a usage.
// Plus this is some serious law of Demeter breaking there...
Set<NameDeclaration> candidates = new HashSet<>(node.getScope().getDeclarations().keySet());
Optional.ofNullable(node.getScope().getParent())
.map(Scope::getDeclarations)
.map(Map::keySet)
.ifPresent(candidates::addAll);
List<NameOccurrence> occurrences =
candidates.stream()
.filter(nd -> node.equals(nd.getNode()))
.findFirst()
.map(nd -> {
// nd.getScope() != nd.getNode().getScope()?? wtf?
List<NameOccurrence> usages = nd.getNode().getScope().getDeclarations().get(nd);
if (usages == null) {
usages = nd.getNode().getScope().getParent().getDeclarations().get(nd);
}
return usages;
})
.orElse(Collections.emptyList());
parent.highlightAsNameOccurences(occurrences);
}

View File

@ -12,10 +12,14 @@ import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
@ -27,7 +31,10 @@ import org.reactfx.value.Var;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;
import net.sourceforge.pmd.lang.symboltable.ScopedNode;
import net.sourceforge.pmd.util.ClasspathClassLoader;
import net.sourceforge.pmd.util.fxdesigner.app.AbstractController;
import net.sourceforge.pmd.util.fxdesigner.app.CompositeSelectionSource;
@ -79,6 +86,8 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
private final ASTManager astManager;
private final Var<Node> currentFocusNode = Var.newSimpleVar(null);
private final Var<List<Node>> currentRuleResults = Var.newSimpleVar(Collections.emptyList());
private final Var<List<Node>> currentErrorNodes = Var.newSimpleVar(Collections.emptyList());
private final Var<List<File>> auxclasspathFiles = Var.newSimpleVar(emptyList());
private final Val<ClassLoader> auxclasspathClassLoader = auxclasspathFiles.map(fileList -> {
@ -132,12 +141,15 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
codeEditorArea.setParagraphGraphicFactory(lineNumberFactory());
currentRuleResultsProperty().values().subscribe(this::highlightXPathResults);
currentErrorNodesProperty().values().subscribe(this::highlightErrorNodes);
}
@Override
protected void afterParentInit() {
DesignerUtil.rewire(astManager.languageVersionProperty(), languageVersionUIProperty);
moveCaret(0, 0);
}
@ -237,24 +249,6 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
}
/** Clears the error nodes. */
public void clearErrorNodes() {
codeEditorArea.clearStyleLayer(StyleLayerIds.ERROR);
}
/** Clears the name occurrences. */
public void clearNameOccurences() {
codeEditorArea.clearStyleLayer(StyleLayerIds.NAME_OCCURENCE);
}
/** Clears the highlighting of XPath results. */
public void clearXPathHighlight() {
codeEditorArea.clearStyleLayer(StyleLayerIds.XPATH_RESULT);
}
/**
* Highlights the given node (or nothing if null).
* Removes highlighting on the previously highlighted node.
@ -274,23 +268,63 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
// editor is only restyled if the selection has changed
Platform.runLater(() -> codeEditorArea.styleNodes(node == null ? emptyList() : singleton(node), StyleLayerIds.FOCUS, true));
if (node instanceof ScopedNode) {
// not null as well
Platform.runLater(() -> highlightNameOccurrences(getNameOccurrences((ScopedNode) node)));
}
}
private List<NameOccurrence> getNameOccurrences(ScopedNode node) {
// For MethodNameDeclaration the scope is the method scope, which is not the scope it is declared
// in but the scope it declares! That means that getDeclarations().get(declaration) returns null
// and no name occurrences are found. We thus look in the parent, but ultimately the name occurrence
// finder is broken since it can't find e.g. the use of a method in another scope. Plus in case of
// overloads both overloads are reported to have a usage.
// Plus this is some serious law of Demeter breaking there...
Set<NameDeclaration> candidates = new HashSet<>(node.getScope().getDeclarations().keySet());
Optional.ofNullable(node.getScope().getParent())
.map(Scope::getDeclarations)
.map(Map::keySet)
.ifPresent(candidates::addAll);
return candidates.stream()
.filter(nd -> node.equals(nd.getNode()))
.findFirst()
.map(nd -> {
// nd.getScope() != nd.getNode().getScope()?? wtf?
List<NameOccurrence> usages = nd.getNode().getScope().getDeclarations().get(nd);
if (usages == null) {
usages = nd.getNode().getScope().getParent().getDeclarations().get(nd);
}
return usages;
})
.orElse(Collections.emptyList());
}
/** Highlights xpath results (xpath highlight). */
public void highlightXPathResults(Collection<? extends Node> nodes) {
private void highlightXPathResults(Collection<? extends Node> nodes) {
codeEditorArea.styleNodes(nodes, StyleLayerIds.XPATH_RESULT, true);
}
/** Highlights name occurrences (secondary highlight). */
public void highlightNameOccurrences(Collection<? extends NameOccurrence> occs) {
private void highlightNameOccurrences(Collection<? extends NameOccurrence> occs) {
codeEditorArea.styleNodes(occs.stream().map(NameOccurrence::getLocation).collect(Collectors.toList()), StyleLayerIds.NAME_OCCURENCE, true);
}
/** Highlights nodes that are in error (secondary highlight). */
public void highlightErrorNodes(Collection<? extends Node> nodes) {
private void highlightErrorNodes(Collection<? extends Node> nodes) {
codeEditorArea.styleNodes(nodes, StyleLayerIds.ERROR, true);
if (!nodes.isEmpty()) {
scrollEditorToNode(nodes.iterator().next());
@ -298,6 +332,16 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
}
public Var<List<Node>> currentRuleResultsProperty() {
return currentRuleResults;
}
public Var<List<Node>> currentErrorNodesProperty() {
return currentErrorNodes;
}
/** Scroll the editor to a node and makes it visible. */
private void scrollEditorToNode(Node node) {
@ -318,13 +362,8 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
}
public void clearStyleLayers() {
codeEditorArea.clearStyleLayers();
}
/** Moves the caret to a position and makes the view follow it. */
public void moveCaret(int line, int column) {
private void moveCaret(int line, int column) {
codeEditorArea.moveTo(line, column);
codeEditorArea.requestFollowCaret();
}

View File

@ -98,6 +98,8 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
@FXML
private ListView<TextAwareNodeWrapper> xpathResultListView;
private final Var<List<Node>> currentResults = Var.newSimpleVar(Collections.emptyList());
// ui property
private Var<String> xpathVersionUIProperty = Var.newSimpleVar(XPathRuleQuery.XPATH_2_0);
@ -261,7 +263,7 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
xpath,
ruleBuilder.getRuleProperties()));
xpathResultListView.setItems(results.stream().map(parent::wrapNode).collect(Collectors.toCollection(LiveArrayList::new)));
parent.highlightXPathResults(results);
this.currentResults.setValue(results);
violationsTitledPane.setTitle("Matched nodes (" + results.size() + ")");
// Notify that everything went OK so we can avoid logging very recent exceptions
raiseParsableXPathFlag();
@ -280,7 +282,7 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
public void invalidateResults(boolean error) {
xpathResultListView.getItems().clear();
parent.resetXPathResults();
this.currentResults.setValue(Collections.emptyList());
violationsTitledPane.setTitle("Matched nodes" + (error ? "\t(error)" : ""));
}
@ -309,6 +311,9 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
dialog.show();
}
public Val<List<Node>> currentResultsProperty() {
return currentResults;
}
public String getXpathExpression() {
return xpathExpressionArea.getText();

View File

@ -4,16 +4,16 @@
package net.sourceforge.pmd.util.fxdesigner.popups;
import static org.reactfx.EventStreams.valuesOf;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.kordamp.ikonli.javafx.FontIcon;
import org.reactfx.EventStreams;
import org.reactfx.Subscription;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
@ -175,25 +175,12 @@ public final class EventLogController extends AbstractController<MainDesignerCon
private Subscription bindPopupToThisController() {
Subscription binding =
EventStreams.valuesOf(eventLogTableView.getSelectionModel().selectedItemProperty())
valuesOf(eventLogTableView.getSelectionModel().selectedItemProperty())
.distinct()
.subscribe(this::onExceptionSelectionChanges);
binding = binding.and(
EventStreams.valuesOf(eventLogTableView.focusedProperty())
.successionEnds(Duration.ofMillis(100))
.subscribe(b -> {
if (b) {
parent.handleSelectedNodeInError(selectedErrorNodes.getValue());
} else {
parent.resetSelectedErrorNodes();
}
})
);
binding = binding.and(
selectedErrorNodes.values().subscribe(parent::handleSelectedNodeInError)
);
// reset error nodes on closing
binding = binding.and(() -> selectedErrorNodes.setValue(Collections.emptyList()));
SortedList<LogEntry> logEntries = new SortedList<>(getLogger().getLog(), Comparator.reverseOrder());
eventLogTableView.itemsProperty().setValue(logEntries);
@ -211,8 +198,9 @@ public final class EventLogController extends AbstractController<MainDesignerCon
private void handleSelectedEntry(LogEntry entry) {
if (entry == null) {
selectedErrorNodes.setValue(Collections.emptyList());
if (entry == null) {
return;
}
@ -224,14 +212,15 @@ public final class EventLogController extends AbstractController<MainDesignerCon
}
public void showPopup() {
public void showPopup(Subscription extSub) {
myPopupStage.show();
popupBinding = bindPopupToThisController();
popupBinding = bindPopupToThisController().and(extSub);
eventLogTableView.refresh();
myPopupStage.setOnCloseRequest(e -> hidePopup());
}
public void hidePopup() {
private void hidePopup() {
myPopupStage.hide();
popupBinding.unsubscribe();
popupBinding = () -> {};
@ -244,6 +233,10 @@ public final class EventLogController extends AbstractController<MainDesignerCon
}
public Val<List<Node>> errorNodesProperty() {
return selectedErrorNodes;
}
private Val<String> titleProperty() {
return parent.getLogger().numNewLogEntriesProperty().map(i -> "Event log (" + (i > 0 ? i : "no") + " new)");
}