From d8b2b8927a6ff9abf31c803a94c3590966be4ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Henrique=20H=C3=BCpner?= Date: Tue, 7 May 2024 09:38:24 -0300 Subject: [PATCH] Report diagnostics in the code editor --- .../java/dev/thihup/jvisualg/ide/Main.java | 152 ++++++++++++++++-- .../src/main/java/module-info.java | 5 +- .../dev/thihup/jvisualg/ide/styles.css | 15 +- .../thihup/jvisualg/lsp/VisualgLauncher.java | 12 +- .../src/main/java/module-info.java | 2 + 5 files changed, 165 insertions(+), 21 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 8503f9a..9197da5 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 @@ -1,36 +1,57 @@ package dev.thihup.jvisualg.ide; +import dev.thihup.jvisualg.lsp.VisualgLauncher; import javafx.application.Application; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.geometry.Point2D; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.stage.Popup; import javafx.stage.Stage; +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageServer; import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.LineNumberFactory; +import org.fxmisc.richtext.event.MouseOverTextEvent; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyleSpansBuilder; import org.reactfx.Subscription; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.time.Duration; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.LongAdder; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Main extends Application { - private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final ExecutorService executor = Executors.newFixedThreadPool(3); @FXML private CodeArea codeArea; private Subscription subscribe; + private Launcher clientLauncher; + private Future lspServer; + private Future lspClient; @Override public void start(Stage stage) throws Exception { @@ -77,25 +98,131 @@ public void start(Stage stage) throws Exception { stage.setScene(scene); stage.show(); + + PipedInputStream inClient = new PipedInputStream(); + PipedOutputStream outClient = new PipedOutputStream(); + PipedInputStream inServer = new PipedInputStream(); + PipedOutputStream outServer = new PipedOutputStream(); + + inClient.connect(outServer); + outClient.connect(inServer); + + lspServer = VisualgLauncher.startServer(inServer, outServer, executor); + clientLauncher = LSPLauncher.createClientLauncher(new VisualgLanguageClient(), inClient, outClient, executor, null); + + lspClient = clientLauncher.startListening(); + + Popup popup = new Popup(); + Label popupMsg = new Label(); + popupMsg.setStyle( + "-fx-background-color: black;" + + "-fx-text-fill: white;" + + "-fx-padding: 5;"); + popup.getContent().add(popupMsg); + + codeArea.setMouseOverTextDelay(Duration.ofMillis(200)); + codeArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> { + int chIdx = e.getCharacterIndex(); + Point2D pos = e.getScreenPosition(); + if (codeArea.getText().isEmpty() || diagnostics == null) + 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); + }); + + }); + codeArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> { + popup.hide(); + }); + + if (Boolean.getBoolean("autoClose")) Platform.runLater(stage::close); } + private List diagnostics; + + class VisualgLanguageClient implements LanguageClient { + + + @Override + public void telemetryEvent(Object object) { + + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { + Main.this.diagnostics = diagnostics.getDiagnostics(); + diagnostics.getDiagnostics().forEach(diagnostic -> { + System.out.println(diagnostic.getMessage()); + Platform.runLater(() -> { + if (codeArea.getText().isEmpty()) + return; + int offsetStart = toOffset(codeArea.getText(), diagnostic.getRange().getStart()); + int offsetEnd = toOffset(codeArea.getText(), diagnostic.getRange().getEnd()) + 1; + codeArea.setStyleClass(offsetStart, offsetEnd, "error"); + }); + + }); + } + + @Override + public void showMessage(MessageParams messageParams) { + + } + + @Override + public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { + return null; + } + + @Override + public void logMessage(MessageParams message) { + System.out.println(message.getMessage()); + } + } + @Override public void stop() throws Exception { if (subscribe != null) { subscribe.unsubscribe(); } + if (lspServer != null) { + lspServer.cancel(true); + } + if (lspClient != null) { + lspClient.cancel(true); + } executor.shutdown(); } + private int toOffset(String text, Position position) { + String[] lines = text.split("\n"); + int offset = 0; + for (int i = 0; i < lines.length; i++) { + if (i == position.getLine()) { + return offset + position.getCharacter(); + } + offset += lines[i].length() + 1; + } + return 0; + } + + public static void main(String[] args) { launch(args); } private static final List KEYWORDS = List.of( - "Div", "Mod", "Se", "Entao", "Então", "Senao", "Senão", "FimSe", "Para", "De", "Ate", "Até", "Passo", "FimPara", "Faca", "Faça", "Enquanto", "FimEnquanto", "Retorne", "E", "Ou", "Nao", "Não", "Escolha", "FimEscolha", "Repita", "FimRepita", "Caso", "OutroCaso", + "Div", "Mod", "Se", "Entao", "Então", "Senao", "Senão", "FimSe", "Para", "De", "Ate", "Até", "Passo", "FimPara", "Faca", "Faça", "Enquanto", "FimEnquanto", "Retorne", "E", "Ou", "Nao", "Não", "Escolha", "FimEscolha", "Repita", "FimRepita", "Caso", "OutroCaso", "abs", "arccos", "arcsen", "arctan", "asc", "carac", "caracpnum", "compr", "copia", "cos", "cotan", "exp", "grauprad", "int", "log", "logn", "maiusc", "minusc", "numpcarac", "pos", "pi", "quad", "radpgrau", "raizq", "rand", "randi" ); @@ -105,7 +232,7 @@ public static void main(String[] args) { ); private static final List SPECIAL_KEYWORDS = List.of( - "Escreva", "Escreval", "Leia", "Algoritmo", "FimAlgoritmo", "Funcao", "Função", "FimFuncao", "FimFunção", "Procedimento", "FimProcedimento","Inicio", "Var" + "Escreva", "Escreval", "Leia", "Algoritmo", "FimAlgoritmo", "Funcao", "Função", "FimFuncao", "FimFunção", "Procedimento", "FimProcedimento", "Inicio", "Var" ); @@ -119,17 +246,22 @@ public static void main(String[] args) { private static final Pattern PATTERN = Pattern.compile( "(?" + KEYWORD_PATTERN + ")" - + "|(?" + STRING_PATTERN + ")" - + "|(?" + COMMENT_PATTERN + ")" - + "|(?" + NUMBER_PATTERN + ")" - + "|(?" + TYPES_PATTERN + ")" - + "|(?" + SPECIAL_PATTERN + ")"); + + "|(?" + STRING_PATTERN + ")" + + "|(?" + COMMENT_PATTERN + ")" + + "|(?" + NUMBER_PATTERN + ")" + + "|(?" + TYPES_PATTERN + ")" + + "|(?" + SPECIAL_PATTERN + ")"); + + private final LongAdder counter = new LongAdder(); private Task>> computeHighlightingAsync() { String text = codeArea.getText(); Task>> task = new Task<>() { @Override protected StyleSpans> call() throws Exception { + VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier("untitled://file", counter.intValue()); + DidChangeTextDocumentParams didChangeTextDocumentParams = new DidChangeTextDocumentParams(versionedTextDocumentIdentifier, Collections.singletonList(new TextDocumentContentChangeEvent(text))); + clientLauncher.getRemoteProxy().getTextDocumentService().didChange(didChangeTextDocumentParams); return computeHighlighting(text); } }; @@ -145,7 +277,7 @@ private StyleSpans> computeHighlighting(String text) { Matcher matcher = PATTERN.matcher(text); int lastKwEnd = 0; StyleSpansBuilder> spansBuilder = new StyleSpansBuilder<>(); - while(matcher.find()) { + while (matcher.find()) { List style = switch (matcher) { case Matcher _ when matcher.group("KEYWORD") != null -> List.of("keyword"); 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 533a265..59803d2 100644 --- a/dev.thihup.jvisualg.ide/src/main/java/module-info.java +++ b/dev.thihup.jvisualg.ide/src/main/java/module-info.java @@ -4,7 +4,10 @@ requires javafx.fxml; requires org.fxmisc.richtext; requires reactfx; - + requires dev.thihup.jvisualg.lsp; + requires org.eclipse.lsp4j; + requires org.eclipse.lsp4j.jsonrpc; + requires javafx.controls; exports dev.thihup.jvisualg.ide to javafx.graphics; opens dev.thihup.jvisualg.ide to javafx.fxml; diff --git a/dev.thihup.jvisualg.ide/src/main/resources/dev/thihup/jvisualg/ide/styles.css b/dev.thihup.jvisualg.ide/src/main/resources/dev/thihup/jvisualg/ide/styles.css index 58bf85d..b2719ef 100644 --- a/dev.thihup.jvisualg.ide/src/main/resources/dev/thihup/jvisualg/ide/styles.css +++ b/dev.thihup.jvisualg.ide/src/main/resources/dev/thihup/jvisualg/ide/styles.css @@ -1,9 +1,15 @@ -.text0 { - -fx-fill: #000000; +#codeArea { + -fx-font-family: "Courier New"; + -fx-font-size: 10pt; } -.background0 { - -fx-background-color: #FFFFFF; +.error { + -rtfx-background-color: #f0f0f0; + -rtfx-underline-color: red; + -rtfx-underline-dash-array: 2 2; + -rtfx-underline-width: 1; + -rtfx-underline-cap: butt; + -fx-fill: red; } .italic { @@ -26,7 +32,6 @@ -fx-fill: #FF0000; } - .keyword { -fx-fill: #0000FF; } diff --git a/dev.thihup.jvisualg.lsp/src/main/java/dev/thihup/jvisualg/lsp/VisualgLauncher.java b/dev.thihup.jvisualg.lsp/src/main/java/dev/thihup/jvisualg/lsp/VisualgLauncher.java index 01e28a2..8a09642 100644 --- a/dev.thihup.jvisualg.lsp/src/main/java/dev/thihup/jvisualg/lsp/VisualgLauncher.java +++ b/dev.thihup.jvisualg.lsp/src/main/java/dev/thihup/jvisualg/lsp/VisualgLauncher.java @@ -4,21 +4,23 @@ import org.eclipse.lsp4j.launch.LSPLauncher; import org.eclipse.lsp4j.services.LanguageClient; -import java.io.*; -import java.util.Timer; -import java.util.TimerTask; +import java.io.InputStream; +import java.io.OutputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class VisualgLauncher { public static void main(String[] args) throws Throwable { + startServer(System.in, System.out, Executors.newSingleThreadExecutor()); + } + public static Future startServer(InputStream inputStream, OutputStream outputStream, ExecutorService executor) { VisualgLanguageServer server = new VisualgLanguageServer(); - Launcher serverLauncher = LSPLauncher.createServerLauncher(server, System.in, System.out, Executors.newSingleThreadExecutor(), null); + Launcher serverLauncher = LSPLauncher.createServerLauncher(server, inputStream, outputStream, executor, null); server.connect(serverLauncher.getRemoteProxy()); - Future voidFuture = serverLauncher.startListening(); + return serverLauncher.startListening(); } } diff --git a/dev.thihup.jvisualg.lsp/src/main/java/module-info.java b/dev.thihup.jvisualg.lsp/src/main/java/module-info.java index 8f555be..91aeac6 100644 --- a/dev.thihup.jvisualg.lsp/src/main/java/module-info.java +++ b/dev.thihup.jvisualg.lsp/src/main/java/module-info.java @@ -3,4 +3,6 @@ requires org.eclipse.lsp4j.jsonrpc; requires com.google.gson; requires dev.thihup.jvisualg.frontend; + + exports dev.thihup.jvisualg.lsp to dev.thihup.jvisualg.ide; }