net.sourceforge.pmd.util.fxdesigner.SourceEditorController.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.pmd.util.fxdesigner.SourceEditorController.java

Source

/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.util.fxdesigner;

import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.IntFunction;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.fxmisc.richtext.LineNumberFactory;
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.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;
import net.sourceforge.pmd.util.fxdesigner.popups.AuxclasspathSetupController;
import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper;
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.HighlightLayerCodeArea;
import net.sourceforge.pmd.util.fxdesigner.util.codearea.HighlightLayerCodeArea.LayerId;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ASTTreeCell;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ASTTreeItem;
import net.sourceforge.pmd.util.fxdesigner.util.controls.TreeViewWrapper;

import javafx.css.PseudoClass;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;

/**
 * One editor, i.e. source editor and ast tree view.
 *
 * @author Clment Fournier
 * @since 6.0.0
 */
public class SourceEditorController implements Initializable, SettingsOwner {

    private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100);

    @FXML
    private Label astTitleLabel;
    @FXML
    private TreeView<Node> astTreeView;
    @FXML
    private HighlightLayerCodeArea<StyleLayerIds> codeEditorArea;

    private ASTManager astManager;
    private TreeViewWrapper<Node> treeViewWrapper;

    private final MainDesignerController parent;

    private Var<Node> currentFocusNode = Var.newSimpleVar(null);
    private ASTTreeItem selectedTreeItem;

    private Var<List<File>> auxclasspathFiles = Var.newSimpleVar(emptyList());
    private final Val<ClassLoader> auxclasspathClassLoader = auxclasspathFiles.map(fileList -> {
        try {
            return new ClasspathClassLoader(fileList, SourceEditorController.class.getClassLoader());
        } catch (IOException e) {
            e.printStackTrace();
            return SourceEditorController.class.getClassLoader();
        }
    });

    public SourceEditorController(DesignerRoot owner, MainDesignerController mainController) {
        parent = mainController;
        astManager = new ASTManager(owner);

    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        treeViewWrapper = new TreeViewWrapper<>(astTreeView);
        astTreeView.setCellFactory(treeView -> new ASTTreeCell(parent));

        languageVersionProperty().values().filterMap(Objects::nonNull, LanguageVersion::getLanguage).distinct()
                .subscribe(this::updateSyntaxHighlighter);

        EventStreams.valuesOf(astTreeView.getSelectionModel().selectedItemProperty())
                .filterMap(Objects::nonNull, TreeItem::getValue).subscribe(parent::onNodeItemSelected);

        codeEditorArea.plainTextChanges().filter(t -> !t.isIdentity()).successionEnds(AST_REFRESH_DELAY)
                // Refresh the AST anytime the text, classloader, or language version changes
                .or(auxclasspathClassLoader.changes()).or(languageVersionProperty().changes()).subscribe(tick -> {
                    // Discard the AST if the language version has changed
                    tick.ifRight(c -> astTreeView.setRoot(null));
                    parent.refreshAST();
                });

        codeEditorArea.setParagraphGraphicFactory(lineNumberFactory());
    }

    private IntFunction<javafx.scene.Node> lineNumberFactory() {
        IntFunction<javafx.scene.Node> base = LineNumberFactory.get(codeEditorArea);
        Val<Integer> activePar = Val.wrap(codeEditorArea.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;
        };
    }

    /**
     * Refreshes the AST and returns the new compilation unit if the parse didn't fail.
     */
    public Optional<Node> refreshAST() {
        String source = getText();

        if (StringUtils.isBlank(source)) {
            astTreeView.setRoot(null);
            return Optional.empty();
        }

        Optional<Node> current;

        try {
            current = astManager.updateIfChanged(source, auxclasspathClassLoader.getValue());
        } catch (ParseAbortedException e) {
            astTitleLabel.setText("Abstract syntax tree (error)");
            return Optional.empty();
        }

        current.ifPresent(this::setUpToDateCompilationUnit);
        return current;
    }

    public void showAuxclasspathSetupPopup(DesignerRoot root) {
        new AuxclasspathSetupController(root).show(root.getMainStage(), auxclasspathFiles.getValue(),
                auxclasspathFiles::setValue);
    }

    private void setUpToDateCompilationUnit(Node node) {
        parent.invalidateAst();
        astTitleLabel.setText("Abstract syntax tree");
        ASTTreeItem root = ASTTreeItem.getRoot(node);
        astTreeView.setRoot(root);
    }

    private void updateSyntaxHighlighter(Language language) {
        codeEditorArea
                .setSyntaxHighlighter(AvailableSyntaxHighlighters.getHighlighterForLanguage(language).orElse(null));
    }

    /** Clears the name occurences. */
    public void clearErrorNodes() {
        codeEditorArea.clearStyleLayer(StyleLayerIds.ERROR);
    }

    /** Clears the name occurences. */
    public void clearNameOccurences() {
        codeEditorArea.clearStyleLayer(StyleLayerIds.ERROR);
    }

    /** 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.
     */
    public void setFocusNode(Node node) {
        if (Objects.equals(node, currentFocusNode.getValue())) {
            return;
        }

        codeEditorArea.styleNodes(node == null ? emptyList() : singleton(node), StyleLayerIds.FOCUS, true);

        if (node != null) {
            scrollEditorToNode(node);
        }

        currentFocusNode.setValue(node);
    }

    /** Highlights xpath results (xpath highlight). */
    public 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) {
        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) {
        codeEditorArea.styleNodes(nodes, StyleLayerIds.ERROR, true);
        if (!nodes.isEmpty()) {
            scrollEditorToNode(nodes.iterator().next());
        }
    }

    /** Scroll the editor to a node and makes it visible. */
    private void scrollEditorToNode(Node node) {

        codeEditorArea.moveTo(node.getBeginLine() - 1, 0);

        int visibleLength = codeEditorArea.lastVisibleParToAllParIndex()
                - codeEditorArea.firstVisibleParToAllParIndex();

        if (node.getEndLine() - node.getBeginLine() > visibleLength
                || node.getBeginLine() < codeEditorArea.firstVisibleParToAllParIndex()) {
            codeEditorArea.showParagraphAtTop(Math.max(node.getBeginLine() - 2, 0));
        } else if (node.getEndLine() > codeEditorArea.lastVisibleParToAllParIndex()) {
            codeEditorArea
                    .showParagraphAtBottom(Math.min(node.getEndLine(), codeEditorArea.getParagraphs().size()));
        }
    }

    public void clearStyleLayers() {
        codeEditorArea.clearStyleLayers();
    }

    public void focusNodeInTreeView(Node node) {
        SelectionModel<TreeItem<Node>> selectionModel = astTreeView.getSelectionModel();

        // node is different from the old one
        if (selectedTreeItem == null && node != null
                || selectedTreeItem != null && !Objects.equals(node, selectedTreeItem.getValue())) {
            ASTTreeItem found = ((ASTTreeItem) astTreeView.getRoot()).findItem(node);
            if (found != null) {
                selectionModel.select(found);
            }
            selectedTreeItem = found;

            astTreeView.getFocusModel().focus(selectionModel.getSelectedIndex());
            if (!treeViewWrapper.isIndexVisible(selectionModel.getSelectedIndex())) {
                astTreeView.scrollTo(selectionModel.getSelectedIndex());
            }
        }
    }

    /** Moves the caret to a position and makes the view follow it. */
    public void moveCaret(int line, int column) {
        codeEditorArea.moveTo(line, column);
        codeEditorArea.requestFollowCaret();
    }

    public TextAwareNodeWrapper wrapNode(Node node) {
        return codeEditorArea.wrapNode(node);
    }

    @PersistentProperty
    public LanguageVersion getLanguageVersion() {
        return astManager.getLanguageVersion();
    }

    public void setLanguageVersion(LanguageVersion version) {
        astManager.setLanguageVersion(version);
    }

    public Var<LanguageVersion> languageVersionProperty() {
        return astManager.languageVersionProperty();
    }

    /**
     * Returns the most up-to-date compilation unit, or empty if it can't be parsed.
     */
    public Optional<Node> getCompilationUnit() {
        return astManager.getCompilationUnit();
    }

    @PersistentProperty
    public String getText() {
        return codeEditorArea.getText();
    }

    public void setText(String expression) {
        codeEditorArea.replaceText(expression);
    }

    public Val<String> textProperty() {
        return Val.wrap(codeEditorArea.textProperty());
    }

    @PersistentProperty
    public String getAuxclasspathFiles() {
        return auxclasspathFiles.getValue().stream().map(File::getAbsolutePath)
                .collect(Collectors.joining(File.pathSeparator));
    }

    public void setAuxclasspathFiles(String files) {
        List<File> newVal = Arrays.stream(files.split(File.pathSeparator)).map(File::new)
                .collect(Collectors.toList());
        auxclasspathFiles.setValue(newVal);
    }

    /** Style layers for the code area. */
    private 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;
        }
    }

}