Extract node edition code area

This commit is contained in:
Clément Fournier 2019-01-29 18:32:54 +01:00
parent 4829404a67
commit bb2999378f
4 changed files with 308 additions and 239 deletions

View File

@ -13,11 +13,13 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
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.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
@ -42,6 +44,10 @@ import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.Parser;
import net.sourceforge.pmd.lang.rule.xpath.XPathRuleQuery;
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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
@ -411,4 +417,37 @@ public final class DesignerUtil {
}
public static 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());
}
}

View File

@ -0,0 +1,226 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.util.fxdesigner.util.controls;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import org.fxmisc.richtext.LineNumberFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.ScopedNode;
import net.sourceforge.pmd.util.fxdesigner.SourceEditorController;
import net.sourceforge.pmd.util.fxdesigner.app.DesignerRoot;
import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource;
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.AvailableSyntaxHighlighters;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.HighlightLayerCodeArea;
import net.sourceforge.pmd.util.fxdesigner.util.controls.NodeEditionCodeArea.StyleLayerIds;
import javafx.application.Platform;
import javafx.css.PseudoClass;
/**
* A layered code area made to display nodes. Handles the presentation of nodes in place of {@link SourceEditorController}.
*
* @author Clément Fournier
*/
public class NodeEditionCodeArea extends HighlightLayerCodeArea<StyleLayerIds> implements NodeSelectionSource {
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<NameOccurrence>> currentNameOccurrences = Var.newSimpleVar(Collections.emptyList());
private DesignerRoot designerRoot;
public NodeEditionCodeArea() {
super(StyleLayerIds.class);
setParagraphGraphicFactory(lineNumberFactory());
currentRuleResultsProperty().values().subscribe(this::highlightXPathResults);
currentErrorNodesProperty().values().subscribe(this::highlightErrorNodes);
currentNameOccurrences.values().subscribe(this::highlightNameOccurrences);
}
/** Scroll the editor to a node and makes it visible. */
private void scrollToNode(Node node) {
moveTo(node.getBeginLine() - 1, 0);
if (getVisibleParagraphs().size() < 1) {
return;
}
int visibleLength = lastVisibleParToAllParIndex() - firstVisibleParToAllParIndex();
if (node.getEndLine() - node.getBeginLine() > visibleLength
|| node.getBeginLine() < firstVisibleParToAllParIndex()) {
showParagraphAtTop(Math.max(node.getBeginLine() - 2, 0));
} else if (node.getEndLine() > lastVisibleParToAllParIndex()) {
showParagraphAtBottom(Math.min(node.getEndLine(), getParagraphs().size()));
}
}
private IntFunction<javafx.scene.Node> lineNumberFactory() {
IntFunction<javafx.scene.Node> base = LineNumberFactory.get(this);
Val<Integer> activePar = Val.wrap(currentParagraphProperty());
return idx -> {
javafx.scene.Node label = base.apply(idx);
activePar.conditionOnShowing(label)
.values()
.subscribe(p -> label.pseudoClassStateChanged(PseudoClass.getPseudoClass("has-caret"), idx == p));
// adds a pseudo class if part of the focus node appears on this line
currentFocusNode.conditionOnShowing(label)
.values()
.subscribe(n -> label.pseudoClassStateChanged(PseudoClass.getPseudoClass("is-focus-node"),
n != null && idx + 1 <= n.getEndLine() && idx + 1 >= n.getBeginLine()));
return label;
};
}
public final Var<List<Node>> currentRuleResultsProperty() {
return currentRuleResults;
}
public final Var<List<Node>> currentErrorNodesProperty() {
return currentErrorNodes;
}
public Var<List<NameOccurrence>> currentNameOccurrencesProperty() {
return currentNameOccurrences;
}
/** Highlights xpath results (xpath highlight). */
private void highlightXPathResults(Collection<? extends Node> nodes) {
styleNodes(nodes, StyleLayerIds.XPATH_RESULT, true);
}
/** Highlights name occurrences (secondary highlight). */
private void highlightNameOccurrences(Collection<? extends NameOccurrence> occs) {
styleNodes(occs.stream().map(NameOccurrence::getLocation).collect(Collectors.toList()), StyleLayerIds.NAME_OCCURENCE, true);
}
/** Highlights nodes that are in error (secondary highlight). */
private void highlightErrorNodes(Collection<? extends Node> nodes) {
styleNodes(nodes, StyleLayerIds.ERROR, true);
if (!nodes.isEmpty()) {
scrollToNode(nodes.iterator().next());
}
}
/** Moves the caret to a position and makes the view follow it. */
public void moveCaret(int line, int column) {
moveTo(line, column);
requestFollowCaret();
}
@Override
public EventStream<NodeSelectionEvent> getSelectionEvents() {
// never emits selection events itself for now
return EventStreams.never();
}
@Override
public void setFocusNode(Node node) {
// editor is always scrolled when re-selecting a node
if (node != null) {
Platform.runLater(() -> scrollToNode(node));
}
if (Objects.equals(node, currentFocusNode.getValue())) {
return;
}
currentFocusNode.setValue(node);
// editor is only restyled if the selection has changed
Platform.runLater(() -> styleNodes(node == null ? emptyList() : singleton(node), StyleLayerIds.FOCUS, true));
if (node instanceof ScopedNode) {
// not null as well
Platform.runLater(() -> highlightNameOccurrences(DesignerUtil.getNameOccurrences((ScopedNode) node)));
}
}
@Override
public DesignerRoot getDesignerRoot() {
return designerRoot;
}
public void setDesignerRoot(DesignerRoot designerRoot) {
this.designerRoot = designerRoot;
}
public void updateSyntaxHighlighter(Language language) {
setSyntaxHighlighter(AvailableSyntaxHighlighters.getHighlighterForLanguage(language).orElse(null));
}
/** Style layers for the code area. */
enum StyleLayerIds implements LayerId {
// caution, the name of the constants are used as style classes
/** For the currently selected node. */
FOCUS,
/** For declaration usages. */
NAME_OCCURENCE,
/** For nodes in error. */
ERROR,
/** For xpath results. */
XPATH_RESULT;
private final String styleClass; // the id will be used as a style class
StyleLayerIds() {
this.styleClass = name().toLowerCase(Locale.ROOT).replace('_', '-') + "-highlight";
}
/** focus-highlight, xpath-highlight, error-highlight, name-occurrence-highlight */
@Override
public String getStyleClass() {
return styleClass;
}
}
}

View File

@ -4,8 +4,8 @@
<?import org.kordamp.ikonli.javafx.FontIcon?>
<?import net.sourceforge.pmd.util.fxdesigner.util.codearea.HighlightLayerCodeArea?>
<?import net.sourceforge.pmd.util.fxdesigner.util.controls.AstTreeView?>
<?import net.sourceforge.pmd.util.fxdesigner.util.controls.NodeEditionCodeArea?>
<?import net.sourceforge.pmd.util.fxdesigner.util.controls.ToolbarTitledPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.MenuButton?>
@ -45,15 +45,13 @@
</MenuButton>
</toolbarItems>
<content>
<HighlightLayerCodeArea fx:id="codeEditorArea"
idEnum="net.sourceforge.pmd.util.fxdesigner.SourceEditorController$StyleLayerIds"
stylesheets="@../css/editor-theme.css"
BorderPane.alignment="CENTER">
<NodeEditionCodeArea fx:id="nodeEditionCodeArea"
stylesheets="@../css/editor-theme.css"
BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</HighlightLayerCodeArea>
<!--<TextArea />-->
</NodeEditionCodeArea>
</content>
</ToolbarTitledPane>
</children>