Highlight error nodes from the event log panel

This commit is contained in:
Clément Fournier committed 2018-06-16 00:15:38 +02:00
1 parent 5241ebf3cd
commit 0fd7f282f5
11 files changed
+233 -96

No files matched your search

@@ -56,7 +56,7 @@ public class Designer extends Application {
NodeInfoPanelController nodeInfoPanelController = new NodeInfoPanelController(mainController);
XPathPanelController xpathPanelController = new XPathPanelController(owner, mainController);
SourceEditorController sourceEditorController = new SourceEditorController(owner, mainController);
EventLogController eventLogController = new EventLogController(owner);
EventLogController eventLogController = new EventLogController(owner, mainController);
loader.setControllerFactory(type -> {
if (type == MainDesignerController.class) {
@@ -8,16 +8,22 @@ import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ResourceBundle;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.value.Var;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry;
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category;
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableCell;
@@ -41,6 +47,7 @@ public class EventLogController implements Initializable {
private static final Duration PARSE_EXCEPTION_DELAY = Duration.ofMillis(3000);
private final DesignerRoot designerRoot;
private final MainDesignerController mediator;
@FXML
private TableView<LogEntry> eventLogTableView;
@@ -53,9 +60,12 @@ public class EventLogController implements Initializable {
@FXML
private TextArea logDetailsTextArea;
private Var<List<Node>> selectedErrorNodes = Var.newSimpleVar(Collections.emptyList());
public EventLogController(DesignerRoot owner) {
public EventLogController(DesignerRoot owner, MainDesignerController mediator) {
this.designerRoot = owner;
this.mediator = mediator;
}
@@ -64,8 +74,7 @@ public class EventLogController implements Initializable {
logCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("category"));
logMessageColumn.setCellValueFactory(new PropertyValueFactory<>("message"));
final DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
logDateColumn.setCellValueFactory(
entry -> new SimpleObjectProperty<>(entry.getValue().getTimestamp()));
logDateColumn.setCellValueFactory(entry -> new SimpleObjectProperty<>(entry.getValue().getTimestamp()));
logDateColumn.setCellFactory(column -> new TableCell<LogEntry, Date>() {
@Override
protected void updateItem(Date item, boolean empty) {
@@ -96,12 +105,25 @@ public class EventLogController implements Initializable {
eventLogTableView.getSelectionModel()
.selectedItemProperty()
.addListener((obs, oldVal, newVal) -> logDetailsTextArea.setText(
newVal == null ? "" : newVal.getStackTrace()));
.addListener(this::onExceptionSelectionChanges);
EventStreams.combine(EventStreams.changesOf(eventLogTableView.focusedProperty()),
EventStreams.changesOf(selectedErrorNodes));
EventStreams.valuesOf(eventLogTableView.focusedProperty())
.successionEnds(Duration.ofMillis(100))
.subscribe(b -> {
if (b) {
mediator.handleSelectedNodeInError(selectedErrorNodes.getValue());
} else {
mediator.resetSelectedErrorNodes();
}
});
selectedErrorNodes.values().subscribe(mediator::handleSelectedNodeInError);
eventLogTableView.resizeColumn(logMessageColumn, -1);
logMessageColumn.prefWidthProperty()
.bind(eventLogTableView.widthProperty()
.subtract(logCategoryColumn.getPrefWidth())
@@ -110,4 +132,37 @@ public class EventLogController implements Initializable {
logDateColumn.setSortType(SortType.DESCENDING);
}
private void handleSelectedEntry(LogEntry entry) {
if (entry == null) {
selectedErrorNodes.setValue(Collections.emptyList());
return;
}
switch (entry.getCategory()) {
case OTHER:
break;
case PARSE_EXCEPTION:
// TODO
break;
case TYPERESOLUTION_EXCEPTION:
case SYMBOL_FACADE_EXCEPTION:
DesignerUtil.stackTraceToXPath(entry.getThrown()).map(mediator::runXPathQuery).ifPresent(selectedErrorNodes::setValue);
break;
default:
break;
}
}
private void onExceptionSelectionChanges(ObservableValue<? extends LogEntry> obs, LogEntry oldVal, LogEntry newVal) {
if (newVal != null) {
logDetailsTextArea.setText(newVal.getStackTrace());
} else {
logDetailsTextArea.clear();
}
if (newVal != oldVal) {
handleSelectedEntry(newVal);
}
}
}
@@ -12,9 +12,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Stack;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.reactfx.value.Val;
@@ -22,7 +22,7 @@ import org.reactfx.value.Val;
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.util.fxdesigner.model.XPathEvaluationException;
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
import net.sourceforge.pmd.util.fxdesigner.util.LimitedSizeStack;
import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner;
@@ -245,24 +245,49 @@ public class MainDesignerController implements Initializable, SettingsOwner {
*/
public void onNodeItemSelected(Node selectedValue) {
nodeInfoPanelController.displayInfo(selectedValue);
// The following line causes problems, since it wipes out the name occurrence highlighting,
// but it's already fixed in a PR to come soon
sourceEditorController.clearNodeHighlight();
sourceEditorController.highlightNodePrimary(selectedValue);
sourceEditorController.setFocusNode(selectedValue);
sourceEditorController.focusNodeInTreeView(selectedValue);
}
public void onNameDeclarationSelected(NameDeclaration declaration) {
Platform.runLater(() -> onNodeItemSelected(declaration.getNode()));
sourceEditorController.clearSecondaryHighlight();
List<NameOccurrence> occ = declaration.getNode().getScope().getDeclarations().get(declaration);
if (occ != null) {
sourceEditorController.highlightNodesSecondary(occ.stream()
.map(NameOccurrence::getLocation)
.collect(Collectors.toList()));
}
Optional.ofNullable(declaration.getNode().getScope().getDeclarations().get(declaration))
.ifPresent(sourceEditorController::highlightNameOccurences);
sourceEditorController.setFocusNode(declaration.getNode());
}
/**
* Runs an XPath (2.0) query on the current AST.
* Performs no side effects.
*
* @param query the query
* @return the matched nodes
* @throws XPathEvaluationException if the query fails
*/
public List<Node> runXPathQuery(String query) throws XPathEvaluationException {
return xpathPanelController.runXPathQuery(sourceEditorController.getCompilationUnit(),
getLanguageVersion(), query);
}
// TODO consider using a messenger pattern instead of mediator
/**
* 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.clearSecondaryHighlight();
}
@@ -355,7 +380,7 @@ public class MainDesignerController implements Initializable, SettingsOwner {
public void invalidateAst() {
nodeInfoPanelController.invalidateInfo();
xpathPanelController.invalidateResults(false);
sourceEditorController.clearNodeHighlight();
sourceEditorController.clearFocusHighlight();
}
@@ -15,7 +15,6 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@@ -27,6 +26,7 @@ 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.NameOccurrence;
import net.sourceforge.pmd.util.ClasspathClassLoader;
import net.sourceforge.pmd.util.fxdesigner.model.ASTManager;
import net.sourceforge.pmd.util.fxdesigner.model.ParseAbortedException;
@@ -35,6 +35,7 @@ import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner;
import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.AvailableSyntaxHighlighters;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea.LayerId;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.SyntaxHighlighter;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ASTTreeCell;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ASTTreeItem;
@@ -76,8 +77,8 @@ public class SourceEditorController implements Initializable, SettingsOwner {
return new ClasspathClassLoader(fileList, SourceEditorController.class.getClassLoader());
} catch (IOException e) {
e.printStackTrace();
return SourceEditorController.class.getClassLoader();
}
return SourceEditorController.class.getClassLoader();
});
public SourceEditorController(DesignerRoot owner, MainDesignerController mainController) {
@@ -185,34 +186,50 @@ public class SourceEditorController implements Initializable, SettingsOwner {
}
public void clearNodeHighlight() {
codeEditorArea.clearPrimaryStyleLayer();
/** Clears the focus node highlight. */
public void clearFocusHighlight() {
codeEditorArea.clearStyleLayer(LayerId.FOCUS);
}
public void highlightNodePrimary(Node node) {
highlightNodes(Collections.singleton(node), Collections.singleton("primary-highlight"));
/** Clears the secondary highlight. Doesn't clear the primary focus.. */
public void clearSecondaryHighlight() {
codeEditorArea.clearStyleLayer(LayerId.SECONDARY);
}
private void highlightNodes(Collection<? extends Node> nodes, Set<String> cssClasses) {
/** Highlights the given node. Removes highlighting on the previously highlighted node. */
public void setFocusNode(Node node) {
clearFocusHighlight();
highlightNodes(Collections.singleton(node), LayerId.FOCUS);
}
/** Highlights name occurences (secondary highlight). */
public void highlightNameOccurences(Collection<? extends NameOccurrence> occs) {
clearSecondaryHighlight();
highlightNodes(occs.stream().map(NameOccurrence::getLocation).collect(Collectors.toList()), LayerId.SECONDARY, "name-occurence");
}
/** Highlights nodes that are in error (secondary highlight). */
public void highlightErrorNodes(Collection<? extends Node> nodes) {
clearSecondaryHighlight();
highlightNodes(nodes, LayerId.SECONDARY, "error-highlight");
}
private void highlightNodes(Collection<? extends Node> nodes, LayerId layer, String... cssClasses) {
for (Node node : nodes) {
if (codeEditorArea.isInRange(node)) {
codeEditorArea.styleCss(node, cssClasses);
codeEditorArea.styleCss(node, layer, cssClasses);
codeEditorArea.paintCss();
codeEditorArea.moveTo(node.getBeginLine() - 1, 0);
codeEditorArea.requestFollowCaret();
} else {
codeEditorArea.clearPrimaryStyleLayer();
}
}
}
public void highlightNodesSecondary(Collection<? extends Node> nodes) {
highlightNodes(nodes, Collections.singleton("secondary-highlight"));
}
public void focusNodeInTreeView(Node node) {
SelectionModel<TreeItem<Node>> selectionModel = astTreeView.getSelectionModel();
@@ -186,7 +186,10 @@ public class XPathPanelController implements Initializable, SettingsOwner {
/**
* Evaluate XPath on the given compilation unit.
* Evaluate the contents of the XPath expression area
* on the given compilation unit. This updates the xpath
* result panel, and can log XPath exceptions to the
* event log panel.
*
* @param compilationUnit The AST root
* @param version The language version
@@ -220,6 +223,11 @@ public class XPathPanelController implements Initializable, SettingsOwner {
}
public List<Node> runXPathQuery(Node compilationUnit, LanguageVersion version, String query) throws XPathEvaluationException {
return xpathEvaluator.evaluateQuery(compilationUnit, version, "2.0", query, ruleBuilder.getRuleProperties());
}
public void invalidateResults(boolean error) {
xpathResultListView.getItems().clear();
violationsTitledPane.setText("Matched nodes" + (error ? "\t(error)" : ""));
@@ -29,6 +29,10 @@ public class LogEntry {
}
public Throwable getThrown() {
return throwable;
}
public String getMessage() {
return throwable.getMessage();
}
@@ -30,7 +30,8 @@ public class XPathEvaluator {
/**
* Evaluates an XPath query on the compilation unit.
* Evaluates an XPath query on the compilation unit. Performs
* no side effects.
*
* @param compilationUnit AST root
* @param languageVersion language version
@@ -5,6 +5,8 @@
package net.sourceforge.pmd.util.fxdesigner.util;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -214,4 +216,21 @@ public final class DesignerUtil {
return lines.isEmpty() ? Optional.empty() : Optional.of("//" + String.join("/", lines));
}
/**
* Works out an xpath query that matches the node
* which was being visited during the failure.
*
* @param e Exception
*
* @return A query, if possible.
*
* @see #stackTraceToXPath(String)
*/
public static Optional<String> stackTraceToXPath(Throwable e) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
return stackTraceToXPath(writer.toString());
}
}
@@ -46,7 +46,7 @@ public class RestorePropertyVisitor extends BeanNodeVisitor<SettingsOwner> {
}
Map<String, PropertyDescriptor> descriptors = Arrays.stream(PropertyUtils.getPropertyDescriptors(target))
.filter(d -> d.getReadMethod().isAnnotationPresent(PersistentProperty.class))
.filter(d -> d.getReadMethod() != null && d.getReadMethod().isAnnotationPresent(PersistentProperty.class))
.collect(Collectors.toMap(PropertyDescriptor::getName, d -> d));
for (Entry<String, Object> saved : model.getSettingsValues().entrySet()) {
@@ -4,7 +4,12 @@
package net.sourceforge.pmd.util.fxdesigner.util.codearea;
import static net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea.LayerId.FOCUS;
import static net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea.LayerId.SECONDARY;
import static net.sourceforge.pmd.util.fxdesigner.util.codearea.CustomCodeArea.LayerId.XPATH_RESULTS;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
@@ -27,10 +32,12 @@ import javafx.concurrent.Task;
/**
* Code area that can handle syntax highlighting as well as regular node highlighting. Regular node highlighting is
* handled in the "primary" {@link StyleLayer}, which you can affect with {@link #styleCss(Node, Set)}, {@link
* #clearPrimaryStyleLayer()} and the like. Syntax highlighting uses another internal style layer. Syntax highlighting
* is performed asynchronously by another thread. You must shut down the executor gracefully by calling {@link
* #disableSyntaxHighlighting()} before exiting the application.
* handled in several {@link StyleLayer}s, which you can affect with {@link #styleCss(Node, LayerId, String...)},
* {@link #clearStyleLayer(LayerId)} and the like. Highlighting
*
* <p>Syntax highlighting uses another internal style layer. Syntax highlighting
* is performed asynchronously by another thread. You must shut down the executor
* gracefully by calling {@link #disableSyntaxHighlighting()} before exiting the application.
*
* @author Clément Fournier
* @since 6.0.0
@@ -38,7 +45,7 @@ import javafx.concurrent.Task;
public class CustomCodeArea extends CodeArea {
private static final String SYNTAX_HIGHLIGHT_LAYER_ID = "syntax";
private static final String PRIMARY_HIGHLIGHT_LAYER_ID = "primary";
private ExecutorService executorService;
private Subscription syntaxAutoRefresh;
private BooleanProperty isSyntaxHighlightingEnabled = new SimpleBooleanProperty(false);
@@ -49,54 +56,40 @@ public class CustomCodeArea extends CodeArea {
public CustomCodeArea() {
super();
styleContext = new StyleContext(this);
styleContext.addLayer(PRIMARY_HIGHLIGHT_LAYER_ID, new StyleLayer(PRIMARY_HIGHLIGHT_LAYER_ID, this));
styleContext.addLayer(XPATH_RESULTS.id, new StyleLayer(XPATH_RESULTS.id, this));
styleContext.addLayer(SYNTAX_HIGHLIGHT_LAYER_ID, new StyleLayer(SYNTAX_HIGHLIGHT_LAYER_ID, this));
styleContext.addLayer(FOCUS.id, new StyleLayer(FOCUS.id, this));
styleContext.addLayer(SECONDARY.id, new StyleLayer(SECONDARY.id, this));
}
/**
* Styles the region delimited by the coordinates with the given css classes. Should be followed by a call to {@link
* #paintCss()} to update the visual appearance.
* Styles the node in the given layer.
*
* @param beginLine Begin line
* @param beginColumn Begin column
* @param endLine End line
* @param endColumn End column
* @param cssClasses The css classes to apply
*
* @throws IllegalArgumentException if the region identified by the coordinates is out of bounds
* <p>The focus layer is meant for the node in primary focus, in contrast
* with the secondary layer, which highlights some nodes that are related
* to a specific selection (eg error nodes correspond to an error, name
* occurrences correspond to a name declaration). The XPath result layer
* highlights nodes that are independent from any selection (they depend
* on the xpath results).
* @param node node to style
* @param layerId Layer id
* @param cssClasses css classes to apply
*/
public void styleCss(int beginLine, int beginColumn, int endLine, int endColumn, Set<String> cssClasses) {
Set<String> fullClasses = new HashSet<>(cssClasses);
public void styleCss(Node node, LayerId layerId, String... cssClasses) {
Set<String> fullClasses = new HashSet<>(Arrays.asList(cssClasses));
fullClasses.add("text");
fullClasses.add("styled-text-area");
styleContext.getLayer(PRIMARY_HIGHLIGHT_LAYER_ID).style(beginLine, beginColumn, endLine, endColumn, fullClasses);
fullClasses.add(layerId.id + "-highlight"); // focus-highlight, xpath-highlight, secondary-highlight
styleContext.getLayer(layerId.id).style(node.getBeginLine(), node.getBeginColumn(), node.getEndLine(), node.getEndColumn(), fullClasses);
}
/**
* Styles the node's position with the given css classes.
* Clears a style layer.
*
* @param node The node to style
* @param cssClasses The css classes to apply
*
* @throws IllegalArgumentException if the node's coordinates are out of bounds
* @param id layer id.
*/
public void styleCss(Node node, Set<String> cssClasses) {
this.styleCss(node.getBeginLine(), node.getBeginColumn(), node.getEndLine(), node.getEndColumn(), cssClasses);
}
/**
* Replaces the styling of the primary layer by styling the node's position with the given css classes.
*
* @param node The node to style
* @param cssClasses The css classes to apply
*
* @throws IllegalArgumentException if the node's coordinates are out of bounds
*/
public void restylePrimaryStyleLayer(Node node, Set<String> cssClasses) {
clearPrimaryStyleLayer();
styleCss(node, cssClasses);
public void clearStyleLayer(LayerId id) {
styleContext.getLayer(id.id).clearStyles();
}
@@ -109,18 +102,11 @@ public class CustomCodeArea extends CodeArea {
*/
public boolean isInRange(Node n) {
return n.getEndLine() <= getParagraphs().size()
&& (n.getEndLine() != getParagraphs().size()
|| n.getEndColumn() <= getParagraph(n.getEndLine() - 1).length());
&& (n.getEndLine() != getParagraphs().size()
|| n.getEndColumn() <= getParagraph(n.getEndLine() - 1).length());
}
/**
* Clears the primary style layer from its contents.
*/
public void clearPrimaryStyleLayer() {
styleContext.getLayer(PRIMARY_HIGHLIGHT_LAYER_ID).clearStyles();
}
/**
* Clears all style layers from their contents.
@@ -198,12 +184,6 @@ public class CustomCodeArea extends CodeArea {
isSyntaxHighlightingEnabled.set(true);
Objects.requireNonNull(newHighlighter, "The syntax highlighting highlighter cannot be null");
StyleLayer syntaxHighlightLayer = styleContext.getLayer(SYNTAX_HIGHLIGHT_LAYER_ID);
if (syntaxHighlightLayer == null) {
styleContext.addLayer(SYNTAX_HIGHLIGHT_LAYER_ID, new StyleLayer(SYNTAX_HIGHLIGHT_LAYER_ID, this));
}
ObservableList<String> styleClasses = this.getStyleClass();
if (syntaxHighlighter != null) {
styleClasses.remove("." + syntaxHighlighter.getLanguageTerseName());
@@ -261,4 +241,19 @@ public class CustomCodeArea extends CodeArea {
}
/** Public style layers of the code area. */
public enum LayerId {
/** For the currently selected node. */
FOCUS("focus"),
/** For nodes in error, declaration usages. */
SECONDARY("secondary"),
/** For xpath results. */
XPATH_RESULTS("xpath");
private final String id;
LayerId(String id) {
this.id = id;
}
}
}
@@ -1,15 +1,28 @@
/* Presets for the editor theme and syntax highlighting. */
.styled-text-area .primary-highlight {
/* CSS reference of the code area:
https://github.com/FXMisc/RichTextFX/wiki/RichTextFX-CSS-Reference-Guide
*/
.styled-text-area .focus-highlight {
-fx-font-weight: bolder !important;
-fx-fill: royalblue !important;
}
.styled-text-area .secondary-highlight {
.styled-text-area .name-occurence {
-fx-font-weight: bolder !important;
-fx-fill: lightgreen !important;
}
/* Highlights nodes in error */
.styled-text-area .error-highlight {
-rtfx-background-color: lightcoral;
-fx-font-weight: bolder !important;
-fx-fill: darkred !important;
}
/********/
/* Base */
/*******/