diff --git a/org.lflang.diagram/build.gradle b/org.lflang.diagram/build.gradle index bd14ca4a2c..820847b05b 100644 --- a/org.lflang.diagram/build.gradle +++ b/org.lflang.diagram/build.gradle @@ -19,6 +19,7 @@ dependencies { exclude group: 'org.eclipse.platform', module: 'org.eclipse.swt.*' exclude group: 'org.eclipse.platform', module: 'org.eclipse.swt' } + implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.9' } apply plugin: 'application' diff --git a/org.lflang.diagram/src/org/lflang/diagram/lsp/CppLanguageServer.java b/org.lflang.diagram/src/org/lflang/diagram/lsp/CppLanguageServer.java new file mode 100644 index 0000000000..778e859cf8 --- /dev/null +++ b/org.lflang.diagram/src/org/lflang/diagram/lsp/CppLanguageServer.java @@ -0,0 +1,274 @@ +package org.lflang.diagram.lsp; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; +import org.lflang.generator.CodeMap; +import java.nio.charset.StandardCharsets; +import java.io.OutputStream; +import java.io.InputStream; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; +import java.util.Collections; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.MarkedString; +import java.util.Map; +import org.lflang.generator.Position; +import org.lflang.generator.Range; +import java.util.NavigableMap; +import java.util.HashMap; +import java.util.HashSet; +import java.io.File; + +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverParams; +import org.eclipse.xtext.ide.server.hover.IHoverService; +import org.eclipse.xtext.util.CancelIndicator; + +import org.lflang.generator.GeneratorResult; + +public class CppLanguageServer{ + static OutputStream stdin = null; + static Process pr; + static InputStream stdout = null; + static BufferedReader reader = null; + static BufferedWriter writer = null; + static BufferedReader errorReader = null; + static Path generatedPath; + static String generatedDirectory; + static Map storedGeneratedResults; + static Map storedBuildResults; + + + public static void init() { + process_init(); + clangd_init(); + readOutput(); + storedBuildResults = new HashMap(); + System.out.println("Initialized language server"); + } + + public static Hover hoverRequest(HoverParams params) { + Path lfPath = Paths.get(params.getTextDocument().getUri().substring(5)); + if (!storedBuildResults.containsKey(lfPath)) { + Hover preHover = new Hover(); + List> content = Collections.singletonList(Either.forLeft("You first need to build a project")); + preHover.setContents(content); + return preHover; + } + try { + Map result; + GeneratorResult genResult; + ProjectBuildResult buildResult = storedBuildResults.get(lfPath); + result = buildResult.getGeneratorResult().getCodeMaps(); + generatedDirectory = buildResult.getGeneratorResult().getCommand().directory().toString() + "/src-gen"; + check_and_add_cmake_file(); + Position codePos = findPosition(params.getPosition().getLine(), params.getPosition().getCharacter(), lfPath, result); + if (codePos.getOneBasedLine() == 1 && codePos.getOneBasedColumn() == 1) { + return IHoverService.EMPTY_HOVER; // hover to LF code + } + if (!buildResult.isOpen(generatedPath)) { + buildResult.open(generatedPath); + } + return clangdHover(codePos); + } catch (Exception e) { + return IHoverService.EMPTY_HOVER; // Fail silently + } + } + + private static Position findPosition(int line, int col, Path lfFile, Map generatedResult) { + for (Path sourcePath : generatedResult.keySet()) { + CodeMap codeMap = generatedResult.get(sourcePath); + for (int brutLine = 1; brutLine <= 500; brutLine++) { // TODO change brute force positions + for (int brutChar = 1; brutChar <= 100; brutChar++) { + Position generatedPos = Position.fromOneBased(brutLine, brutChar); + Position p = codeMap.adjusted(lfFile, generatedPos); + if (p.getOneBasedLine() == line && p.getOneBasedColumn() == col) { + generatedPath = sourcePath; + return generatedPos; + } + } + } + } + return Position.fromOneBased(1, 1); + } + + private static Hover clangdHover(Position params) { + send_hover_request(params.getOneBasedLine(), params.getOneBasedColumn()); + Hover resultHover = new Hover(); + try { + String jsonResponse = readOutput(); + jsonResponse = jsonResponse.substring(jsonResponse.indexOf(System.getProperty("line.separator"))+1);// remove first line since contains the header + jsonResponse = jsonResponse.substring(jsonResponse.indexOf(System.getProperty("line.separator"))+1); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + String jsonValue = jsonNode.get("result").get("contents").get("value").textValue(); + List> content = Collections.singletonList(Either.forLeft(jsonValue)); + resultHover.setContents(content); + // int lvalue_c = jsonNode.get("result").get("range").get("start").get("character").intValue(); + // int lvalue_l = jsonNode.get("result").get("range").get("start").get("line").intValue(); + // int rvalue_c = jsonNode.get("result").get("range").get("end").get("character").intValue(); + // int rvalue_l = jsonNode.get("result").get("range").get("end").get("line").intValue(); + return resultHover; + } catch (Exception e) { + // most often gets here because there is no fields that jsonNode.get() is looking for (i.e. result is null) + return IHoverService.EMPTY_HOVER; + } + } + + private static void check_and_add_cmake_file() { + try { + File cmake_file = new File(generatedDirectory + "compile_commands.json"); + if (!cmake_file.exists()) { + ProcessBuilder cmake_command = new ProcessBuilder("cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=1"); + cmake_command.directory(new File(generatedDirectory)); + cmake_command.start(); + } + } catch (Exception e) { + System.out.println("failed creating cmake file"); + } + } + + private static String readOutput() { + char[] chars = new char[8192]; + try { + for(int len; (len = reader.read(chars)) > 0 && reader.ready();) { + + } + } catch (Exception e) { + System.out.println("failed reading output"); + } + return String.valueOf(chars); + } + + private static void process_init() { + try { + ProcessBuilder clangd = new ProcessBuilder("clangd"); + pr = clangd.start(); + + stdin = pr.getOutputStream(); + stdout = pr.getInputStream(); + reader = new BufferedReader (new InputStreamReader(stdout)); + writer = new BufferedWriter(new OutputStreamWriter(stdin)); + + errorReader = new BufferedReader(new InputStreamReader(pr.getErrorStream())); + } catch (Exception e) { + System.out.println("failed process init"); + } + } + + private static void readErrorOutput() { + try { + String str = null; + while ((str = errorReader.readLine()) != null && errorReader.ready()) { + System.out.println(str); + } + } catch (Exception e) { + System.out.println("failed reading error output"); + } + } + + private static void send_hover_request(int line, int character) { + try { + String body = "{\"method\": \"textDocument/hover\",\"jsonrpc\": \"2.0\",\"id\": 1,\"params\": {\"textDocument\": {\"uri\": \"file://" + generatedPath.toString() + "\"} ,\"position\": {\"line\": "+line+",\"character\": "+character+"}}}\r\n"; + String header = "Content-Length: " + body.length() +"\r\n\r\n"; + String cmd = header + body; + writer.write(cmd); + writer.flush(); + } catch (Exception e) { + System.out.println("failed hover request"); + } + + } + + private static void clangd_init() { + try { + String body = "{\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {\"rootUri\": \"file://" + generatedDirectory + "\", \"capabilities\": {\"hover\": {\"dynamicRegistration\": false, \"contentFormat\": []}}}}\r\n"; + String header = "Content-Length: " + body.length() +"\r\n\r\n"; + String cmd = header + body; + writer.write(cmd); + writer.flush(); + } catch (Exception e) { + System.out.println("failed clangd init"); + } + } + + private static void did_open_clangd(Path path) { + + try { + String fileContent = Files.readString(path, StandardCharsets.UTF_8); + fileContent = fileContent.replaceAll("\"", "\\\\\""); + fileContent = fileContent.replaceAll("\n", "\\\\n"); + String body = "{\"method\": \"textDocument/didOpen\", \"jsonrpc\": \"2.0\", \"params\": {\"textDocument\": {\"uri\": \"file://" + path + "\", \"languageId\": \"cpp\", \"version\": 1, \"text\": \""+fileContent+"\"}}}\r\n"; + String header = "Content-Length: " + (body.length()) +"\r\n\r\n"; + String cmd = header + body; + writer.write(cmd); + writer.flush(); + } catch (Exception e) { + System.out.println("failed clangd did open"); + } + + } + + private static void did_close_clangd(String filePath) { + try { + String body = "{\"method\": \"textDocument/didClose\", \"jsonrpc\": \"2.0\", \"params\": {\"textDocument\": {\"uri\": \"file://" + filePath + "\"}}}\r\n"; + String header = "Content-Length: " + (body.length()) +"\r\n\r\n"; + String cmd = header + body; + writer.write(cmd); + writer.flush(); + } catch (Exception e) { + System.out.println("failed clangd did close"); + } + } + + static class ProjectBuildResult { + Path uri; + GeneratorResult generatorResult; + HashSet clangOpenFiles; + + public ProjectBuildResult(Path u, GeneratorResult gr) { + uri = u; + generatorResult = gr; + clangOpenFiles = new HashSet(); + } + public GeneratorResult getGeneratorResult() { + return generatorResult; + } + public String path() { + return uri.toString(); + } + + public boolean isOpen(Path p) { + return clangOpenFiles.contains(p); + } + public void open(Path p) { + did_open_clangd(p); + readOutput(); + clangOpenFiles.add(p); + } + public void closeOpenFiles() { + for (Path gen_path : clangOpenFiles) { + did_close_clangd(gen_path.toString()); + readOutput(); + } + } + + } + + public static void addBuild(Path uri, GeneratorResult generatorResult) { + ProjectBuildResult buildResult = new ProjectBuildResult(uri, generatorResult); + if (storedBuildResults.containsKey(uri)) { + // update build if was re-built + ProjectBuildResult result = storedBuildResults.get(uri); + result.closeOpenFiles(); + storedBuildResults.remove(uri); + } + storedBuildResults.put(uri, buildResult); + } +} \ No newline at end of file diff --git a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServer.java b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServer.java index 085247028c..32da33c1a7 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServer.java +++ b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServer.java @@ -8,6 +8,8 @@ import org.eclipse.xtext.ide.server.hover.IHoverService; import org.eclipse.xtext.util.CancelIndicator; +import org.lflang.diagram.lsp.CppLanguageServer; + /** * The Lingua Franca language and diagram server. * @@ -18,6 +20,7 @@ public class LFLanguageServer extends KGraphLanguageServerExtension { public void cancelProgress(WorkDoneProgressCancelParams params) { Progress.cancel(params.getToken().getRight().intValue()); } + boolean initialized = false; @Override protected Hover hover(HoverParams params, CancelIndicator cancelIndicator) { @@ -25,10 +28,11 @@ protected Hover hover(HoverParams params, CancelIndicator cancelIndicator) { // upstream of the ungraceful handling (IndexOutOfBoundsException) of said mistake. This patch is applied here // simply because it is easy. This would be done differently were it not for the fact that we plan to rebuild // this infrastructure from scratch anyway. - try { - return super.hover(params, cancelIndicator); - } catch (IndexOutOfBoundsException e) { - return IHoverService.EMPTY_HOVER; // Fail silently + if (!initialized) { + CppLanguageServer.init(); + initialized = true; } + return CppLanguageServer.hoverRequest(params); + // return IHoverService.EMPTY_HOVER; // Fail silently } } diff --git a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java index 7436a157a4..a591579f5f 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java +++ b/org.lflang.diagram/src/org/lflang/diagram/lsp/LFLanguageServerExtension.java @@ -1,6 +1,7 @@ package org.lflang.diagram.lsp; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.ArrayList; @@ -35,6 +36,9 @@ class LFLanguageServerExtension implements ILanguageServerExtension { /** The access point for reading documents, communicating with the language client, etc. */ private LanguageClient client; + private static GeneratorResult buildResult = null; + private static int buildToken = 0; + private static Path buildPath = null; @Override public void initialize(ILanguageServerAccess access) { @@ -76,6 +80,7 @@ public CompletableFuture build(String uri) { */ @JsonNotification("generator/partialBuild") public void partialBuild(String uri) { + System.out.println("building on " + uri); if (client == null) return; buildWithProgress(client, uri, false); } @@ -99,6 +104,18 @@ public CompletableFuture buildAndRun(String uri) { }); } + public static GeneratorResult getGeneratorResult() { + return buildResult; + } + + public static int getBuildToken() { + return buildToken; + } + + public static Path getBuildPath() { + return buildPath; + } + /** * Describes a build process that has a progress. */ @@ -123,6 +140,11 @@ private GeneratorResult buildWithProgress(LanguageClient client, String uri, boo } finally { progress.end(result == null ? "An internal error occurred." : result.getUserMessage()); } + + // buildResult = result; + // buildToken += 1; + // buildPath = Paths.get(uri.substring(5)); // remove "file:" + CppLanguageServer.addBuild(Paths.get(uri.substring(5)), result); return result; } } diff --git a/org.lflang/src/org/lflang/generator/CodeMap.java b/org.lflang/src/org/lflang/generator/CodeMap.java index 09a0533eb0..73b1b127b5 100644 --- a/org.lflang/src/org/lflang/generator/CodeMap.java +++ b/org.lflang/src/org/lflang/generator/CodeMap.java @@ -241,6 +241,10 @@ public String getGeneratedCode() { return generatedCode; } + public Map> getCodeMap() { + return map; + } + /** * Returns the set of all paths to Lingua Franca files * that are known to contain code that corresponds to