From 5cf8b971c2743218a6082043ffa5e0d2f46c01ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Henrique=20H=C3=BCpner?= Date: Fri, 24 May 2024 15:27:03 -0300 Subject: [PATCH] Improve debug experience --- .../java/dev/thihup/jvisualg/ide/Main.java | 202 +++++++++++++----- .../src/main/java/module-info.java | 1 + .../jvisualg/interpreter/Interpreter.java | 86 +++++--- .../jvisualg/interpreter/InterpreterTest.java | 45 ++-- pom.xml | 6 +- 5 files changed, 236 insertions(+), 104 deletions(-) diff --git a/dev.thihup.jvisualg.ide/src/main/java/dev/thihup/jvisualg/ide/Main.java b/dev.thihup.jvisualg.ide/src/main/java/dev/thihup/jvisualg/ide/Main.java index f4daef2..b2cf541 100644 --- a/dev.thihup.jvisualg.ide/src/main/java/dev/thihup/jvisualg/ide/Main.java +++ b/dev.thihup.jvisualg.ide/src/main/java/dev/thihup/jvisualg/ide/Main.java @@ -9,12 +9,18 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Paint; +import javafx.scene.text.Font; +import javafx.scene.text.FontPosture; import javafx.stage.Popup; import javafx.stage.Stage; import org.eclipse.lsp4j.*; @@ -31,6 +37,7 @@ import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.Pipe; +import java.text.BreakIterator; import java.time.Duration; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -38,6 +45,9 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Main extends Application { @@ -92,9 +102,24 @@ public void start(Stage stage) throws Exception { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("gui.fxml")); fxmlLoader.setController(this); Parent root = fxmlLoader.load(); - - codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea)); - + IntFunction nodeIntFunction = LineNumberFactory.get(codeArea); + codeArea.setParagraphGraphicFactory(lineNumber -> { + Label node = (Label) nodeIntFunction.apply(lineNumber); + node.setFont(Font.font("monospace", FontPosture.REGULAR, 13)); + if (breakpointLines.contains(lineNumber)) { + Label label = new Label(" ".repeat(node.getText().length() - 1) + "."); + label.setTextFill(Paint.valueOf("#ff0000")); + label.setAlignment(Pos.BASELINE_RIGHT); + label.setPadding(node.getPadding()); + label.setFont(node.getFont()); + node.setGraphic(label); + node.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + node.setOnMouseClicked(_ -> handleBreakpointChange(lineNumber)); + node.setCursor(Cursor.HAND); + return node; + }); + breakpointLines.add(5); setupSyntaxHighlighting(); setupDefaultText(); @@ -121,32 +146,27 @@ public void start(Stage stage) throws Exception { }); runButton.addEventHandler(ActionEvent.ACTION, _ -> { - Platform.runLater(() -> { - switch (interpreter.state()) { - case InterpreterState.Running _ -> { - } - case InterpreterState.PausedDebug _ -> interpreter.continueExecution(); - case InterpreterState.NotStarted _ -> { - resetExecution(); - - interpreter.run(codeArea.getText(), executor) - .thenRun(this::handleExecutionSuccessfully) - .exceptionally(this::handleExecutionError) - .whenComplete((_, _) -> Platform.runLater(this::removeDebugStyleFromPreviousLine)); - } - case InterpreterState.CompletedExceptionally _, InterpreterState.CompletedSuccessfully _, InterpreterState.ForcedStop _ -> { - } - } - }); + handleStartOrContinue(); }); setupErrorPopup(); - + stage.onCloseRequestProperty().addListener(_ -> dosWindow.close()); if (Boolean.getBoolean("autoClose")) Platform.runLater(stage::close); } + + private void startExecution(InterpreterState interpreterState) { + interpreter.runWithState(codeArea.getText(), interpreterState); + switch (interpreter.state()) { + case InterpreterState.CompletedSuccessfully _ -> this.handleExecutionSuccessfully(); + case InterpreterState.CompletedExceptionally(Throwable e) -> this.handleExecutionError(e); + default -> throw new IllegalStateException("Unexpected value: " + interpreter.state()); + } + Platform.runLater(this::removeDebugStyleFromPreviousLine); + } + private Void handleExecutionError(Throwable e) { if (e.getCause() instanceof CancellationException) { appendOutput("\n*** Execução terminada pelo usuário.", ToWhere.DOS, When.LATER); @@ -172,7 +192,7 @@ private void resetExecution() { appendOutput("Início da execução\n", ToWhere.OUTPUT, When.NOW); interpreter.reset(); previousDebugLine = -1; - breakpointLines.forEach(interpreter::addBreakpoint); + breakpointLines.forEach(location -> interpreter.addBreakpoint(location + 1)); dosContent.setStyle(DEFAULT_CONSOLE_STYLE); dosContent.clear(); @@ -184,8 +204,8 @@ private void updateDebugArea(ProgramState programState) { programState.stack().entrySet().stream() .mapMulti((Map.Entry> entry, Consumer consumer) -> { entry.getValue().forEach((variableName, variableValue) -> { - final String scopeName = entry.getKey().toUpperCase(); - final String variableNameUpperCase = variableName.toUpperCase(); + String scopeName = entry.getKey().toUpperCase(); + String variableNameUpperCase = variableName.toUpperCase(); addDebug(consumer, variableValue, scopeName, variableNameUpperCase); }); }) @@ -405,31 +425,28 @@ private static Optional getValue(InputRequestValue request, String t private void showScene(Stage stage, Parent root) { Scene scene = new Scene(root); - scene.setOnKeyPressed(x -> { - switch (x.getCode()) { + scene.setOnKeyPressed(keyEvent -> { + switch (keyEvent.getCode()) { case F2 -> { - if (x.isControlDown() && interpreter != null) { + if (keyEvent.isControlDown() && interpreter != null) { interpreter.stop(); } } - case F9 -> runButton.fire(); + case F5 -> { + int currentParagraph = codeArea.getCurrentParagraph(); + handleBreakpointChange(currentParagraph); + } + case F9 -> handleStartOrContinue(); case F8 -> { - switch (interpreter.state()) { - case InterpreterState.PausedDebug _ -> { - interpreter.step(); - } - case InterpreterState.NotStarted _, InterpreterState.ForcedStop _, InterpreterState.CompletedExceptionally _, InterpreterState.CompletedSuccessfully _ -> { - breakpointLines.addLast(1); - runButton.fire(); - } - case InterpreterState.Running _ -> { - } - } + handleStep(); } case ESCAPE -> { switch (interpreter.state()) { - case InterpreterState.ForcedStop _, InterpreterState.CompletedExceptionally _, InterpreterState.CompletedSuccessfully _ -> dosWindow.hide(); - case InterpreterState.PausedDebug _, InterpreterState.NotStarted _, InterpreterState.Running _ -> {} + case InterpreterState.ForcedStop _, InterpreterState.CompletedExceptionally _, + InterpreterState.CompletedSuccessfully _ -> dosWindow.hide(); + case InterpreterState.PausedDebug _, InterpreterState.NotStarted _, + InterpreterState.Running _ -> { + } } } default -> { @@ -442,6 +459,47 @@ private void showScene(Stage stage, Parent root) { stage.show(); } + private void handleBreakpointChange(int currentParagraph) { + if (breakpointLines.contains(currentParagraph)) { + breakpointLines.remove((Integer) currentParagraph); + interpreter.removeBreakpoint(currentParagraph + 1); + } else { + breakpointLines.add(currentParagraph); + interpreter.addBreakpoint(currentParagraph + 1); + } + codeArea.recreateParagraphGraphic(currentParagraph); + } + + private void handleStartOrContinue() { + switch (interpreter.state()) { + case InterpreterState.PausedDebug _ -> { + interpreter.continueExecution(); + } + case InterpreterState.NotStarted _, InterpreterState.ForcedStop _, + InterpreterState.CompletedExceptionally _, InterpreterState.CompletedSuccessfully _ -> { + resetExecution(); + Thread.startVirtualThread(() -> startExecution(InterpreterState.Running.INSTANCE)); + } + case InterpreterState.Running _ -> { + } + } + } + + private void handleStep() { + switch (interpreter.state()) { + case InterpreterState.PausedDebug _ -> { + interpreter.step(); + } + case InterpreterState.NotStarted _, InterpreterState.ForcedStop _, + InterpreterState.CompletedExceptionally _, InterpreterState.CompletedSuccessfully _ -> { + resetExecution(); + Thread.startVirtualThread(() -> startExecution(new InterpreterState.PausedDebug(1))); + } + case InterpreterState.Running _ -> { + } + } + } + private void setupDefaultText() { codeArea.replaceText(0, 0, """ algoritmo "semnome" @@ -505,20 +563,60 @@ private void setupErrorPopup() { private void handlePopupError(MouseOverTextEvent e, Label popupMsg, Popup popup) { int chIdx = e.getCharacterIndex(); Point2D pos = e.getScreenPosition(); - if (codeArea.getText().isEmpty() || diagnostics == null) + + if (codeArea.getText().isEmpty()) { return; - diagnostics.stream() - .filter(diagnostic -> { - int start = toOffset(codeArea.getText(), diagnostic.getRange().getStart()); - int end = toOffset(codeArea.getText(), diagnostic.getRange().getEnd()); - return chIdx >= start && chIdx <= end; - }) - .findFirst().ifPresent(diagnostic -> { - popupMsg.setText(diagnostic.getMessage()); - popup.show(codeArea, pos.getX(), pos.getY() + 10); - }); + } + if (interpreter.state() instanceof InterpreterState.PausedDebug) { + BreakIterator wordIterator = BreakIterator.getWordInstance(Locale.getDefault()); + Optional first = tryGetValue(wordIterator, chIdx).or(() -> tryGetValue(BreakIterator.getLineInstance(), chIdx)); + if (first.isEmpty()) { + return; + } + DebugState debugState = first.get(); + popupMsg.setText(debugState.getNome + " = " + debugState.getValor()); + popup.show(codeArea, pos.getX(), pos.getY() + 10); + } + if (diagnostics == null) { + diagnostics.stream() + .filter(diagnostic -> { + int start = toOffset(codeArea.getText(), diagnostic.getRange().getStart()); + int end = toOffset(codeArea.getText(), diagnostic.getRange().getEnd()); + return chIdx >= start && chIdx <= end; + }) + .findFirst().ifPresent(diagnostic -> { + popupMsg.setText(diagnostic.getMessage()); + popup.show(codeArea, pos.getX(), pos.getY() + 10); + }); + } } + private Optional tryGetValue(BreakIterator wordIterator, int chIdx) { + wordIterator.setText(codeArea.getText()); + + int start = wordIterator.preceding(chIdx); + int end = wordIterator.following(chIdx); + + if (start == BreakIterator.DONE || end == BreakIterator.DONE) { + return Optional.empty(); + } + + String originalText = codeArea.getText(start, wordIterator.next()); + String text = originalText.strip().replaceAll("[().,+-/%^*]", ""); + Optional first = debugArea.getItems().stream().filter(x -> x.getNome().equalsIgnoreCase(text)).findFirst(); + if (first.isEmpty()) { + Matcher matcher = ARRAY_REGEX.matcher(text); + if (matcher.find()) { + String indice = matcher.group(1); + return debugArea.getItems().stream().filter(x -> x.getNome().equalsIgnoreCase(indice)) + .findFirst() + .map(DebugState::getValor).or(() -> Optional.of(indice)) + .flatMap(index -> debugArea.getItems().stream().filter(x -> x.getNome().equalsIgnoreCase(text.replace(indice, index) )).findFirst()); + } + } + return first; + } + private static Pattern ARRAY_REGEX = Pattern.compile("\\w\\[(\\d|\\w)]"); class VisualgLanguageClient implements LanguageClient { diff --git a/dev.thihup.jvisualg.ide/src/main/java/module-info.java b/dev.thihup.jvisualg.ide/src/main/java/module-info.java index d19bd68..6561970 100644 --- a/dev.thihup.jvisualg.ide/src/main/java/module-info.java +++ b/dev.thihup.jvisualg.ide/src/main/java/module-info.java @@ -7,6 +7,7 @@ requires org.eclipse.lsp4j; requires org.eclipse.lsp4j.jsonrpc; requires dev.thihup.jvisualg.interpreter; + requires org.jspecify; exports dev.thihup.jvisualg.ide to javafx.graphics; diff --git a/dev.thihup.jvisualg.interpreter/src/main/java/dev/thihup/jvisualg/interpreter/Interpreter.java b/dev.thihup.jvisualg.interpreter/src/main/java/dev/thihup/jvisualg/interpreter/Interpreter.java index 5b2f8cf..6f37d0d 100644 --- a/dev.thihup.jvisualg.interpreter/src/main/java/dev/thihup/jvisualg/interpreter/Interpreter.java +++ b/dev.thihup.jvisualg.interpreter/src/main/java/dev/thihup/jvisualg/interpreter/Interpreter.java @@ -15,8 +15,11 @@ import java.math.RoundingMode; import java.text.NumberFormat; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CyclicBarrier; import java.util.function.Consumer; +import java.util.function.Function; import java.util.random.RandomGenerator; import java.util.stream.Collectors; @@ -38,6 +41,7 @@ public class Interpreter { private volatile InterpreterState state = InterpreterState.NotStarted.INSTANCE; private InputState inputState; private boolean eco = false; + private TreeMap lineToAstNode; public Interpreter(IO io, @Nullable Consumer debuggerCallback) { @@ -55,6 +59,10 @@ public void addBreakpoint(int location) { this.breakpoints.add(location); } + public void removeBreakpoint(int location) { + this.breakpoints.remove((Integer) location); + } + public void reset() { stack.clear(); functions.clear(); @@ -68,45 +76,66 @@ public InterpreterState state() { return state; } - public CompletableFuture run(String code, ExecutorService executorService) { - return CompletableFuture.runAsync(() -> { - state = InterpreterState.Running.INSTANCE; - ASTResult parse = VisualgParser.parse(code); - parse.node() - .ifPresentOrElse(this::run, () -> { - throw new RuntimeException("Error parsing code: " + parse.errors().stream().map(x -> x.location() + ":" + x.message()).collect(Collectors.joining("\n"))); - }); + public void run(String code) { + runWithState(code, InterpreterState.Running.INSTANCE); + } - }, executorService).whenComplete((_, exception) -> { + public void runWithState(String code, InterpreterState state) { + try { + this.state = state; + ASTResult parse = VisualgParser.parse(code); + Optional optionalNode = parse.node(); + if (optionalNode.isPresent()) { + Node node = optionalNode.get(); + lineToAstNode = node.visitChildren() + .collect(Collectors.toMap(node2 -> node2.location().orElse(Location.EMPTY).startLine(), + Function.identity(), (_, b) -> b, TreeMap::new)); + + this.run(node); + } else { + failParsing(parse); + } + this.state = InterpreterState.CompletedSuccessfully.INSTANCE; + } catch (Exception exception) { + this.state = new InterpreterState.CompletedExceptionally(exception); + } finally { if (debuggerCallback != null) { debuggerCallback.accept(new ProgramState(0, stack)); } + } + } - state = exception != null ? new InterpreterState.CompletedExceptionally(exception) : InterpreterState.CompletedSuccessfully.INSTANCE; - }); + private static void failParsing(ASTResult parse) { + throw new RuntimeException("Error parsing code: " + parse.errors().stream().map(x -> x.location() + ":" + x.message()).collect(Collectors.joining("\n"))); } private void run(Node node) { try { + int currentLineNumber = node.location().orElse(Location.EMPTY).startLine(); switch (state) { case InterpreterState.ForcedStop _ -> throw new CancellationException("Program was cancelled"); case InterpreterState.CompletedSuccessfully _ -> { return; } - case InterpreterState.PausedDebug _ -> handleDebugCommand(node); - - case InterpreterState.CompletedExceptionally _, InterpreterState.Running _, + case InterpreterState.PausedDebug(int lineNumber) + when lineToAstNode.containsKey(lineNumber) && currentLineNumber == lineNumber -> handleDebugCommand(node); + case InterpreterState.PausedDebug e -> { + handleDebugCommand(node); + setNextLineDebug(e); + } + case InterpreterState.CompletedExceptionally _, InterpreterState.NotStarted _ -> { } + case InterpreterState.Running _ when breakpoints.contains(currentLineNumber) + && lineToAstNode.containsKey(currentLineNumber) -> { + state = new InterpreterState.PausedDebug(currentLineNumber); + handleDebugCommand(node); + } + case InterpreterState.Running _ -> { + } } - int lineNumber = node.location().orElse(Location.EMPTY).startLine(); - if (!(state instanceof InterpreterState.PausedDebug) && breakpoints.contains(lineNumber) && - !(node instanceof Node.CompundNode)) { - state = new InterpreterState.PausedDebug(lineNumber); - handleDebugCommand(node); - } switch (node) { case Node.AlgoritimoNode algoritimoNode -> runAlgoritmo(algoritimoNode); @@ -159,9 +188,11 @@ private void runRegistroDeclaration(Node.RegistroDeclarationNode registroDeclara public void step() { try { - if (state instanceof InterpreterState.PausedDebug) { + if (state instanceof InterpreterState.PausedDebug e) { if (lock.getNumberWaiting() == 1) { lock.await(); + setNextLineDebug(e); + } } } catch (InterruptedException | BrokenBarrierException e) { @@ -169,6 +200,12 @@ public void step() { } } + private void setNextLineDebug(InterpreterState.PausedDebug e) { + state = Optional.ofNullable(lineToAstNode.higherKey(e.lineNumber())) + .map(InterpreterState.PausedDebug::new) + .orElse(InterpreterState.Running.INSTANCE); + } + public void continueExecution() { try { if (state instanceof InterpreterState.PausedDebug) { @@ -609,7 +646,8 @@ private T evaluate(Node.ExpressionNode node) { case Node.EmptyExpressionNode _ -> 0; case Node.ArrayAccessNode arrayAccessNode -> evaluateArrayAccessNode(arrayAccessNode); case Node.MemberAccessNode memberAccessNode -> evaluateMemberAccessNode(memberAccessNode); - case Node.RangeNode _ -> throw new UnsupportedOperationException("RangeNode not implemented"); }; + case Node.RangeNode _ -> throw new UnsupportedOperationException("RangeNode not implemented"); + }; } @@ -986,7 +1024,7 @@ private void assignVariable(String name, Object value, AssignContext context) { stack.reversed().values().stream().filter(m -> m.containsKey(name)).findFirst().ifPresentOrElse(m -> { m.put(name, switch (context) { case ARGUMENT -> assignArgument(value, m.get(name).getClass()); - case SIMPLE -> assignSimple(value, m.get(name).getClass()); + case SIMPLE -> assignSimple(value, m.get(name).getClass()); }); }, () -> { throw new TypeException.VariableNotFound(name); diff --git a/dev.thihup.jvisualg.interpreter/src/test/java/dev/thihup/jvisualg/interpreter/InterpreterTest.java b/dev.thihup.jvisualg.interpreter/src/test/java/dev/thihup/jvisualg/interpreter/InterpreterTest.java index db362de..8d93bed 100644 --- a/dev.thihup.jvisualg.interpreter/src/test/java/dev/thihup/jvisualg/interpreter/InterpreterTest.java +++ b/dev.thihup.jvisualg.interpreter/src/test/java/dev/thihup/jvisualg/interpreter/InterpreterTest.java @@ -12,9 +12,6 @@ import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.random.RandomGenerator; import java.util.stream.Stream; @@ -92,7 +89,7 @@ void test() { escreval(falso) escreval(a) fimalgoritmo - """, Executors.newVirtualThreadPerTaskExecutor()).join(); + """); assertEquals( """ Hello, World! 5 @@ -460,7 +457,7 @@ void testRead() { leia(a) escreval(a) fimalgoritmo - """, Executors.newVirtualThreadPerTaskExecutor()).join(); + """); assertEquals( """ @@ -492,7 +489,7 @@ void testExamples(Path path) throws Throwable { Interpreter interpreter = new Interpreter(io); - interpreter.run(Files.readString(path, StandardCharsets.ISO_8859_1), Executors.newVirtualThreadPerTaskExecutor()).get(10, TimeUnit.SECONDS); + interpreter.run(Files.readString(path, StandardCharsets.ISO_8859_1)); System.out.println(stringBuilder); } @@ -529,13 +526,11 @@ void testExamplesErrors(FileAndError file) throws Throwable { Interpreter interpreter = new Interpreter(io); - CompletableFuture run = interpreter.run(Files.readString(file.path, StandardCharsets.ISO_8859_1), Executors.newVirtualThreadPerTaskExecutor()); - try { - run.join(); - fail(); - } catch (CompletionException e) { - System.out.println(stringBuilder); - assertInstanceOf(file.e, e.getCause()); + interpreter.run(Files.readString(file.path, StandardCharsets.ISO_8859_1)); + System.out.println(stringBuilder); + switch (interpreter.state()) { + case InterpreterState.CompletedExceptionally(Throwable e) -> assertInstanceOf(file.e, e); + default -> fail(); } } @@ -571,12 +566,12 @@ void testBinaryOperands(@CartesianTest.Values(strings = {">", ">=", "<", "<=", " fimalgoritmo """; - CompletableFuture run = interpreter.run(program.formatted(type1, type2, operand), Executors.newVirtualThreadPerTaskExecutor()); - try { - run.join(); - } catch (CompletionException e) { - System.out.println(stringBuilder); - assertInstanceOf(TypeException.InvalidOperand.class, e.getCause()); + interpreter.run(program.formatted(type1, type2, operand)); + System.out.println(stringBuilder); + switch (interpreter.state()) { + case InterpreterState.CompletedSuccessfully _ -> {} + case InterpreterState.CompletedExceptionally(Throwable e) -> assertInstanceOf(TypeException.InvalidOperand.class, e); + default -> fail(); } } @@ -606,12 +601,12 @@ void testUnaryOperands(@CartesianTest.Values(strings = {"+", "-", "nao"}) String fimalgoritmo """; - CompletableFuture run = interpreter.run(program.formatted(type, operand), Executors.newVirtualThreadPerTaskExecutor()); - try { - run.join(); - } catch (CompletionException e) { - System.out.println(stringBuilder); - assertInstanceOf(TypeException.InvalidOperand.class, e.getCause()); + interpreter.run(program.formatted(type, operand)); + System.out.println(stringBuilder); + switch (interpreter.state()) { + case InterpreterState.CompletedSuccessfully _ -> {} + case InterpreterState.CompletedExceptionally(Throwable e) -> assertInstanceOf(TypeException.InvalidOperand.class, e); + default -> fail(); } } diff --git a/pom.xml b/pom.xml index 0ed0d1e..f3d79ba 100644 --- a/pom.xml +++ b/pom.xml @@ -29,9 +29,9 @@ 0.3.0 - org.junit.jupiter - junit-jupiter-params - 5.10.2 + org.junit-pioneer + junit-pioneer + 2.2.0 test