Skip to content

Set 'step' location for debugger #165

@bsutton

Description

@bsutton

So I've got a little scripting language for a robotic arm and I'm trying to implement the ability to 'step' through the code.

I need to highlight the current line that the 'debug stepper' is currently on.

I've been trying to adapt code which implements a green arrow to indicate a break point.
The problem is that the code always moves the green arrow to the 'current cursor' location within a CodeArea.

I need to be able to 'select' the line using a 'next step' button.

The pertinent code is:

            scriptEditor = new CodeArea();

                numberFactory = LineNumberFactory.get(scriptEditor);
                IntFunction<Node> arrowFactory = new ArrowFactory(scriptEditor.currentParagraphProperty());
                IntFunction<Node> graphicFactory = line -> {
                    HBox hbox = new HBox(numberFactory.apply(line), arrowFactory.apply(line));
                    hbox.setAlignment(Pos.CENTER_LEFT);
                    return hbox;
                };

                scriptEditor.setParagraphGraphicFactory(graphicFactory);

and the arrow function:

class ArrowFactory implements IntFunction<Node>
    {
        private final ObservableValue<Integer> shownLine;

        ArrowFactory(ObservableValue<Integer> shownLine)
        {
            this.shownLine = shownLine;
        }

        @Override
        public Node apply(int lineNumber)
        {
            Polygon triangle = new Polygon(0.0, 0.0, 10.0, 5.0, 0.0, 10.0);
            triangle.setFill(Color.GREEN);

            ObservableValue<Boolean> visible = Val.map(shownLine, sl -> sl == lineNumber);

            triangle.visibleProperty().bind(Val.flatMap(triangle.sceneProperty(), scene -> {
                return scene != null ? visible : Val.constant(false);
            }));

            return triangle;
        }
    }

I've played with the 'lineNumber' changing it to use a field which contains the current 'step line' but this just results in every line getting an arrow as I step through the code.

Any assistance would be greatly appreciated.

The full class is:

package controller;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.function.IntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.FileChooser;

import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.PlainTextChange;
import org.fxmisc.richtext.StyleSpans;
import org.fxmisc.richtext.StyleSpansBuilder;
import org.reactfx.EventStream;
import org.reactfx.util.Try;
import org.reactfx.value.Val;

import robot.IllegalCommandException;
import robot.InvaidMotorFrequency;
import robot.InvalidMotorConfiguration;
import robot.InvalidMotorException;
import robot.NotConnectedException;
import robot.iRobot;
import application.iDisplay;

public class ScriptController implements iController, Initializable
{

    private static final String[] KEYWORDS = new String[]
    { "mov", "on", "set", "wait", "stop" };

    private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
    private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";

    private static final Pattern PATTERN = Pattern.compile("(?<KEYWORD>" + KEYWORD_PATTERN + ")" + "|(?<COMMENT>"
            + COMMENT_PATTERN + ")");
    @FXML
    TextArea scriptEditorPlaceHolder;

    CodeArea scriptEditor;
    private ExecutorService executor;

    @FXML
    HBox nextCmdLine;

    @FXML
    TextField nextCmd;

    private File activeFile = null;
    private iDisplay display;
    private iRobot robot;
    private File lastDir;

    private MainUIController mainController;

    private int currentStep = 0;

    private IntFunction<Node> numberFactory;

    @Override
    public void init(MainUIController mainController, iDisplay display)
    {
        this.mainController = mainController;

        this.display = display;
        this.robot = mainController.getRobot();
        this.lastDir = robot.getLastSaveDirectory();

        VBox parent = (VBox) scriptEditorPlaceHolder.getParent();

        int pos = 0;
        for (Node child : parent.getChildrenUnmodifiable())
        {
            if (child == scriptEditorPlaceHolder)
            {
                parent.getChildren().remove(child);
                executor = Executors.newSingleThreadExecutor();

                scriptEditor = new CodeArea();

                numberFactory = LineNumberFactory.get(scriptEditor);
                IntFunction<Node> arrowFactory = new ArrowFactory(scriptEditor.currentParagraphProperty());
                IntFunction<Node> graphicFactory = line -> {
                    HBox hbox = new HBox(numberFactory.apply(line), arrowFactory.apply(line));
                    hbox.setAlignment(Pos.CENTER_LEFT);
                    return hbox;
                };

                scriptEditor.setParagraphGraphicFactory(graphicFactory);

                scriptEditor.textProperty().addListener(
                        (obs, oldText, newText) -> {
                            scriptEditor.setStyleSpans(0, computeHighlighting(newText));

                            EventStream<PlainTextChange> textChanges = scriptEditor.plainTextChanges();
                            textChanges.successionEnds(Duration.ofMillis(500))
                                    .supplyTask(this::computeHighlightingAsync).awaitLatest(textChanges).map(Try::get)
                                    .subscribe(this::applyHighlighting);
                        });

                VBox.setVgrow(scriptEditor, Priority.ALWAYS);
                parent.getChildren().add(pos, scriptEditor);
                break;
            }
        }

    }

    private Task<StyleSpans<Collection<String>>> computeHighlightingAsync()
    {
        String text = scriptEditor.getText();
        Task<StyleSpans<Collection<String>>> task = new Task<StyleSpans<Collection<String>>>()
        {
            @Override
            protected StyleSpans<Collection<String>> call() throws Exception
            {
                return computeHighlighting(text);
            }
        };
        executor.execute(task);
        return task;
    }

    private void applyHighlighting(StyleSpans<Collection<String>> highlighting)
    {
        scriptEditor.setStyleSpans(0, highlighting);
    }

    private static StyleSpans<Collection<String>> computeHighlighting(String text)
    {
        Matcher matcher = PATTERN.matcher(text);
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
        while (matcher.find())
        {
            String styleClass = matcher.group("KEYWORD") != null ? "keyword"

            : matcher.group("COMMENT") != null ? "comment" : null; /*
                                                                     * never
                                                                     * happens
                                                                     */
            assert styleClass != null;
            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();
        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources)
    {
        nextCmdLine.setDisable(true);

    }

    @FXML
    void onOpen(ActionEvent event)
    {
        // Create a file chooser
        final FileChooser fc = new FileChooser();
        fc.setInitialDirectory(lastDir);
        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("Robot Move files", "rmf");
        fc.setSelectedExtensionFilter(filter);

        // In response to a button click:
        File openedFile = fc.showOpenDialog(null);

        if (openedFile != null)
        {
            try
            {
                activeFile = openedFile;
                lastDir = activeFile.getParentFile();
                robot.setLastSaveDirectory(fc.getInitialDirectory());

                scriptEditor.clear();
                try (Stream<String> lines = Files.lines(activeFile.toPath()))
                {
                    lines.forEach(s -> scriptEditor.appendText(s + "\n"));
                }
            }
            catch (IOException e)
            {
                display.showException(e);
            }
        }

    }

    @FXML
    void onSave(ActionEvent event)
    {
        if (activeFile != null)
        {
            BufferedWriter writer;
            try
            {
                writer = new BufferedWriter(new FileWriter(activeFile));
                writer.write(scriptEditor.getText());
                writer.close();
            }
            catch (IOException e)
            {
                display.showException(e);
            }

        }
        else
            onSaveAs(event);

    }

    @FXML
    void onSaveAs(ActionEvent event)
    {
        // Create a file chooser
        final FileChooser fc = new FileChooser();
        fc.setInitialDirectory(lastDir);
        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("Robot Move files", "rmf");
        fc.setSelectedExtensionFilter(filter);
        // In response to a button click:
        File savedFile = fc.showSaveDialog(null);

        if (savedFile != null)
        {
            activeFile = savedFile;
            lastDir = activeFile.getParentFile();
            if (!activeFile.getName().endsWith(".rmf"))
                activeFile = new File(activeFile.getParentFile(), activeFile.getName() + ".rmf");
            onSave(event);
            robot.setLastSaveDirectory(fc.getInitialDirectory());
        }

    }

    @FXML
    void onNew(ActionEvent event)
    {
        Alert alert = new Alert(AlertType.CONFIRMATION);
        alert.setTitle("Confirm New action");
        alert.setHeaderText(null);
        alert.setContentText("Are you sure? You will loose any unsaved changes");

        Optional<ButtonType> result = alert.showAndWait();
        if (result.get() == ButtonType.OK)
        {
            activeFile = null;
            scriptEditor.clear();
        }
    }

    @FXML
    void onRun(ActionEvent event)
    {
        runSequence(scriptEditor.getText());
    }

    @FXML
    void onRestart(ActionEvent event)
    {
        currentStep = 0;
        scriptEditor.setStyleClass(0, scriptEditor.getText().length(), "black");
    }

    @FXML
    void onStep(ActionEvent event)
    {
        // not very efficient but means we can handle editing whilst stepping
        String[] cmds = scriptEditor.getText().split("\n");

        if (currentStep == cmds.length)
        {
            scriptEditor.setStyleClass(0, scriptEditor.getText().length(), "black");
            display.showMessage("Sequence complete. Resetting to start");
            currentStep = 0;
        }
        else
        {
            int startCharacter = 0;
            int endCharacter = cmds[0].length();
            // determine character position of current step
            for (int i = 1; i <= currentStep; i++)
            {
                startCharacter = endCharacter + 1;
                endCharacter += cmds[i].length() + 1;
            }

            String cmd = cmds[currentStep].trim();

            scriptEditor.setStyleClass(0, startCharacter, "black");
            scriptEditor.setStyleClass(startCharacter, endCharacter, "blue");
            // scriptEditor.setStyle(startCharacter, endCharacter,
            // "-fx-strikethrough");

            // First time step is click we prime the pump
            if (currentStep >= 0)
            {
                // stepSequence(cmd);
            }

            currentStep++;
            this.nextCmd.setText(cmd);
        }
    }

    private void stepSequence(String nextCommand)
    {
        try
        {
            this.robot.sendCmd(nextCommand, display);
        }
        catch (Exception e1)
        {
            display.showException(e1);
        }
    }

    public void runSequence(String text)
    {
        if (!robot.isConnected())
            this.display.showError("Device is not connected");
        else
        {
            // push into the background so we don't lock the UI.
            ExecutorService executor = Executors.newSingleThreadExecutor();
            executor.submit(() -> {
                try
                {
                    String[] cmds = text.split("\n");

                    int line = 1;
                    for (String cmd : cmds)
                    {
                        scriptEditor.setStyleClass(line, line, ".step-highlite");

                        if (cmd != null)
                            this.robot.sendCmd(cmd.trim(), display);
                        line++;
                    }

                    Platform.runLater(() -> this.display.append("Sequence sent in full\n"));
                }
                catch (NotConnectedException | IOException | TimeoutException | InvalidMotorConfiguration
                        | InvaidMotorFrequency | InvalidMotorException | IllegalCommandException e)
                {
                    Platform.runLater(() -> display.showException(e));
                }
            });
        }

    }

    class ArrowFactory implements IntFunction<Node>
    {
        private final ObservableValue<Integer> shownLine;

        ArrowFactory(ObservableValue<Integer> shownLine)
        {
            this.shownLine = shownLine;
        }

        @Override
        public Node apply(int lineNumber)
        {
            Polygon triangle = new Polygon(0.0, 0.0, 10.0, 5.0, 0.0, 10.0);
            triangle.setFill(Color.GREEN);

            ObservableValue<Boolean> visible = Val.map(shownLine, sl -> sl == lineNumber);

            triangle.visibleProperty().bind(Val.flatMap(triangle.sceneProperty(), scene -> {
                return scene != null ? visible : Val.constant(false);
            }));

            return triangle;
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions