From 28cac9834eb50dcd7299113a00cd48ded2e6ac2c Mon Sep 17 00:00:00 2001 From: JaDogg Date: Sat, 25 May 2024 15:54:52 +0100 Subject: [PATCH] feat(yakshaintellij): show compiler errors --- editor/intellij/gradle.properties | 2 +- .../sdk/language/YakshaAnnotator.java | 3 +- .../sdk/language/YakshaCompilerAnnotator.java | 161 ++++++++++++++++++ .../sdk/language/tw/ExecutableFileState.java | 13 ++ .../tw/ExecutableFileStateService.java | 34 ++++ .../sdk/language/tw/YakshaToolWindow.form | 10 +- .../sdk/language/tw/YakshaToolWindow.java | 45 ++++- .../language/tw/YakshaToolWindowFactory.java | 6 +- .../src/main/resources/META-INF/plugin.xml | 1 + 9 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 editor/intellij/src/main/java/org/intellij/sdk/language/YakshaCompilerAnnotator.java create mode 100644 editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileState.java create mode 100644 editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileStateService.java diff --git a/editor/intellij/gradle.properties b/editor/intellij/gradle.properties index 8abb0d40..1b798e0a 100644 --- a/editor/intellij/gradle.properties +++ b/editor/intellij/gradle.properties @@ -3,7 +3,7 @@ pluginGroup = com.github.jadogg.yakshaintellij pluginName = YakshaIntelliJ # SemVer format -> https://semver.org -pluginVersion = 0.0.12 +pluginVersion = 0.0.13 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 213 diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaAnnotator.java b/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaAnnotator.java index 015e23d2..792da36c 100644 --- a/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaAnnotator.java +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaAnnotator.java @@ -14,6 +14,7 @@ public class YakshaAnnotator implements Annotator { @Override public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) { + if (element instanceof YakshaDataTypeBit) { holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(element.getTextRange()) @@ -35,7 +36,7 @@ public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolde .range(fncall.getFirstChild().getTextRange()) .textAttributes(YakshaSyntaxHighlighter.KEYWORD) .create(); - if ((fullName.equals("cast") || fullName.equals("arrnew") || fullName.equals("array")) && fncall.getArgumentsList() != null + if ((fullName.equals("cast") || fullName.equals("arrnew") || fullName.equals("array") || fullName.equals("fixedarr")) && fncall.getArgumentsList() != null && !fncall.getArgumentsList().isEmpty() && fncall.getArgumentsList().get(0).getExpList() != null && fncall.getArgumentsList().get(0).getExpList().size() > 0) { YakshaExp dt = fncall.getArgumentsList().get(0).getExpList().get(0); diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaCompilerAnnotator.java b/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaCompilerAnnotator.java new file mode 100644 index 00000000..88af02a8 --- /dev/null +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/YakshaCompilerAnnotator.java @@ -0,0 +1,161 @@ +package org.intellij.sdk.language; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.ExternalAnnotator; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import org.intellij.sdk.language.psi.YakshaFile; +import org.intellij.sdk.language.tw.YakshaToolWindow; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class YakshaCompilerAnnotator extends ExternalAnnotator> implements DumbAware { + @Override + public @Nullable YakshaFile collectInformation(@NotNull PsiFile file) { + if (file instanceof YakshaFile) { + return (YakshaFile) file; + } + return null; + } + + @Override + public @NotNull List doAnnotate(@Nullable YakshaFile collectedInfo) { + if (collectedInfo == null) { + return List.of(); + } + final var project = collectedInfo.getProject(); + final var virtualFile = collectedInfo.getVirtualFile(); + File mainFile = findMainFile(new File(virtualFile.getPath()), 5); + if (mainFile == null) { + return List.of(); + } + + + return runCompiler(mainFile); + } + + @Override + public void apply(@NotNull PsiFile file, List annotationResult, @NotNull AnnotationHolder holder) { + if (annotationResult == null || annotationResult.isEmpty()) { + return; + } + + final var document = file.getViewProvider().getDocument(); + if (document == null) { + return; + } + + for (CompilerError error : annotationResult) { + var line = error.line - 1; + if (line < 0 || line >= document.getLineCount()) continue; + + var startOffset = document.getLineStartOffset(line) + error.column - 1; + var endOffset = startOffset + error.length; + + holder.newAnnotation(HighlightSeverity.ERROR, error.message).range(new TextRange(startOffset, endOffset)).create(); + } + } + + private @Nullable Document getDoc(Project project, VirtualFile file) { + final var psiFile = PsiManager.getInstance(project).findFile(file); + if (psiFile == null) { + return null; + } + return PsiDocumentManager.getInstance(project).getDocument(psiFile); + } + + public static class CompilerError { + final String message; + final int line; + final int column; + final int length; + + private CompilerError(final String message, final int line, final int column, final int length) { + this.message = message; + this.line = line; + this.column = column; + this.length = length; + } + } + + private File findMainFile(File dir, int maxDepth) { + File currentDir = dir; + for (int i = 0; i <= maxDepth; i++) { + File mainCFile = new File(currentDir, "main.yaka"); + if (mainCFile.exists() && mainCFile.isFile()) { + return mainCFile; + } + currentDir = currentDir.getParentFile(); + if (currentDir == null) break; + } + return null; + } + + private List runCompiler(File mainFile) { + List errors = new ArrayList<>(); + final var compiler = YakshaToolWindow.YAKSHA_EXE_PATH; + if (compiler.isBlank()) { + System.out.println("------ yaksha compiler path is not set -----"); + return errors; + } + + System.out.println("----- yaksha compiler: " + compiler); + + List command = List.of(compiler, "compile", "-d", mainFile.getAbsolutePath()); + + try { + Process process = new ProcessBuilder(command) + .directory(mainFile.getParentFile()) + .redirectErrorStream(true) + .start(); + + process.waitFor(60, TimeUnit.SECONDS); + + String output = new String(process.getInputStream().readAllBytes()); + int exitCode = process.exitValue(); + + errors.addAll(parseCompilerErrors(output, mainFile.getAbsolutePath())); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + + return errors; + } + + private List parseCompilerErrors(String output, String mainFilePath) { + List errors = new ArrayList<>(); + String[] lines = output.split("\\R"); + + for (String line : lines) { + if (!line.trim().isEmpty()) { + JsonObject json = JsonParser.parseString(line).getAsJsonObject(); + String file = json.has("file") ? json.get("file").getAsString() : mainFilePath; + int lineNumber = json.has("line") ? json.get("line").getAsInt() : 0; + int columnNumber = json.has("pos") ? json.get("pos").getAsInt() : 0; + String message = json.get("message").getAsString(); + int tokenLength = json.has("token") ? json.get("token").getAsString().length() : 1; + + if (mainFilePath.equals(file)) { + errors.add(new CompilerError(message, lineNumber, columnNumber, tokenLength)); + } + } + } + + return errors; + } +} diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileState.java b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileState.java new file mode 100644 index 00000000..a2d7f6ef --- /dev/null +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileState.java @@ -0,0 +1,13 @@ +package org.intellij.sdk.language.tw; + +public final class ExecutableFileState { + private String executableFilePath = ""; + + public String getExecutableFilePath() { + return executableFilePath; + } + + public void setExecutableFilePath(String executableFilePath) { + this.executableFilePath = executableFilePath; + } +} diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileStateService.java b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileStateService.java new file mode 100644 index 00000000..7f34b225 --- /dev/null +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/ExecutableFileStateService.java @@ -0,0 +1,34 @@ +package org.intellij.sdk.language.tw; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@State( + name = "ExecutableFileStateService", + storages = @Storage("ExecutableFileStateService.xml") +) +@Service +public final class ExecutableFileStateService implements PersistentStateComponent { + + private ExecutableFileState state = new ExecutableFileState(); + + @Nullable + @Override + public ExecutableFileState getState() { + return state; + } + + @Override + public void loadState(@NotNull ExecutableFileState state) { + this.state = state; + } + + public static ExecutableFileStateService getInstance(Project project) { + return project.getService(ExecutableFileStateService.class); + } +} diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.form b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.form index 9d0213e8..73ea0638 100644 --- a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.form +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.form @@ -1,6 +1,6 @@
- + @@ -29,6 +29,14 @@ + + + + + + + + diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.java b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.java index 7db7f37a..a51d9a5d 100644 --- a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.java +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindow.java @@ -1,5 +1,6 @@ package org.intellij.sdk.language.tw; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.wm.ToolWindow; import org.intellij.sdk.language.YakshaIcons; import org.intellij.sdk.language.yaksha_docs.YakshaDocs; @@ -11,13 +12,16 @@ import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import java.awt.*; +import java.io.File; +import java.util.Locale; import java.util.concurrent.*; public class YakshaToolWindow { - + public static String YAKSHA_EXE_PATH = ""; private JPanel myToolWindowContent; private JTree documentationTree; private JTextField filterText; + private JButton setYakshaCompilerPathButton; private final Debouncer debouncer = new Debouncer(); @@ -47,7 +51,14 @@ public void shutdown() { } } - public YakshaToolWindow(ToolWindow toolWindow) { + public YakshaToolWindow(ToolWindow toolWindow, ExecutableFileStateService service) { + final var state = service.getState(); + if (state != null) { + final var path = state.getExecutableFilePath(); + if (path != null && !path.isBlank()) { + YAKSHA_EXE_PATH = path; // Set current path when the tool window is opened + } + } final DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { @@ -86,8 +97,38 @@ public void handle() { renderTree(filterText.getText().strip()); } }); + + setYakshaCompilerPathButton.addActionListener(e -> onSelectFileButtonClicked(service)); } + private void onSelectFileButtonClicked(ExecutableFileStateService service) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Select Executable File"); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || (f.isFile() && f.canExecute() && f.getName().toLowerCase(Locale.ROOT).startsWith("yaksha")); + } + + @Override + public String getDescription() { + return "Yaksha executable"; + } + }); + + int returnValue = fileChooser.showOpenDialog(myToolWindowContent); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = fileChooser.getSelectedFile(); + String filePath = selectedFile.getAbsolutePath(); + assert service.getState() != null; + service.getState().setExecutableFilePath(filePath); + YAKSHA_EXE_PATH = filePath; + Messages.showMessageDialog("Set compiler path: " + filePath, "Information", Messages.getInformationIcon()); + } + } + + private void renderTree(String filter) { debouncer.debounce(Void.class, () -> SwingUtilities.invokeLater(() -> { final DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root"); diff --git a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindowFactory.java b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindowFactory.java index ad1108df..604f8269 100644 --- a/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindowFactory.java +++ b/editor/intellij/src/main/java/org/intellij/sdk/language/tw/YakshaToolWindowFactory.java @@ -1,6 +1,5 @@ package org.intellij.sdk.language.tw; -import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; @@ -8,9 +7,10 @@ import com.intellij.ui.content.ContentFactory; import org.jetbrains.annotations.NotNull; -public class YakshaToolWindowFactory implements ToolWindowFactory, DumbAware { +public class YakshaToolWindowFactory implements ToolWindowFactory { public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { - YakshaToolWindow yakshaToolWindow = new YakshaToolWindow(toolWindow); + ExecutableFileStateService service = ExecutableFileStateService.getInstance(project); + YakshaToolWindow yakshaToolWindow = new YakshaToolWindow(toolWindow, service); ContentFactory contentFactory = ContentFactory.SERVICE.getInstance(); Content content = contentFactory.createContent(yakshaToolWindow.getContent(), "", false); toolWindow.getContentManager().addContent(content); diff --git a/editor/intellij/src/main/resources/META-INF/plugin.xml b/editor/intellij/src/main/resources/META-INF/plugin.xml index 8bd47272..e145e1fe 100644 --- a/editor/intellij/src/main/resources/META-INF/plugin.xml +++ b/editor/intellij/src/main/resources/META-INF/plugin.xml @@ -7,6 +7,7 @@ com.intellij.modules.platform +