diff --git a/.gitignore b/.gitignore index afef5c9..976740f 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ fabric.properties bin/ build/ +!build/libs/Souffle_Ide_Plugin-1.0-SNAPSHOT.jar dist/ .gradle/* *.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index b16dfb6..5552de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [0.2.0] +- Added integration with [souffle-lint](https://github.com/langston-barrett/souffle-lint). +- Added source code action to trigger souffle-lint +- Added code action to extract types ## [0.1.6] Added code actions for reformatting documentation comments. diff --git a/README.md b/README.md index 7693994..e91038d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,12 @@ This is a plugin adding basic smart features to the Soufflé language, using the ![Syntax error message](images/syntax_error.png) +- Integration with [souffle-lint](https://github.com/langston-barrett/souffle-lint) (If available on user system) + +![Souffle-lint message](images/souffle-lint.png) + - Hover (Provide declaration info on hovers) + Provide documentation about relations taken from comments (for multiline comments use the /* */ style) ![Hover example](images/hover_1.png) @@ -75,6 +80,12 @@ In libraries with heavy use of the C preprocessor macros, sometimes parsing fail ## Release Notes +### 0.2.0 + +- Added integration with [souffle-lint](https://github.com/langston-barrett/souffle-lint). +- Added source code action to trigger souffle-lint +- Added code action to extract types + ### 0.1.6 Added code actions for reformatting documentation comments. diff --git a/build/libs/Souffle_Ide_Plugin-1.0-SNAPSHOT.jar b/build/libs/Souffle_Ide_Plugin-1.0-SNAPSHOT.jar index 8dba982..37bde4c 100644 Binary files a/build/libs/Souffle_Ide_Plugin-1.0-SNAPSHOT.jar and b/build/libs/Souffle_Ide_Plugin-1.0-SNAPSHOT.jar differ diff --git a/images/souffle-lint.png b/images/souffle-lint.png new file mode 100644 index 0000000..811eddb Binary files /dev/null and b/images/souffle-lint.png differ diff --git a/package.json b/package.json index 23783f7..e4ca6e9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "souffle-lang-server", "displayName": "Soufflé Datalog Language Server", "description": "Soufflé Datalog Language Server. Add smart features to the Soufflé Datalog Language with the help of LSP in a VS code plugin", - "version": "0.1.6", + "version": "0.2.0", "engines": { "vscode": "^1.65.0" }, diff --git a/src/main/java/CodeActionProvider.java b/src/main/java/CodeActionProvider.java new file mode 100644 index 0000000..c3bce93 --- /dev/null +++ b/src/main/java/CodeActionProvider.java @@ -0,0 +1,148 @@ +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import parsing.symbols.SouffleContext; +import parsing.symbols.SouffleProjectContext; +import parsing.symbols.SouffleSymbol; +import parsing.symbols.SouffleSymbolType; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CodeActionProvider { + public CodeActionProvider() { + } + + public List> getCodeAction(CodeActionParams params) { + List> actions = new ArrayList>(); + + lintCodeAction(params, actions); + + Range cursor = params.getRange(); + SouffleContext context = SouffleProjectContext.getInstance().getContext(params.getTextDocument().getUri(), cursor); + if (context != null) { + SouffleSymbol currentSymbol = context.getSymbol(cursor); + if (currentSymbol != null) { + switch (currentSymbol.getKind()) { + case RELATION_USE: + case RELATION_DECL: + generateIOCodeAction(params, actions, context, currentSymbol); + break; + case TYPE_USE: + case TYPE_DECL: + extractTypeCodeAction(params, actions, currentSymbol); + break; + } + } + } + + return actions; + } + + private void extractTypeCodeAction(CodeActionParams params, List> actions, SouffleSymbol currentSymbol) { + CodeAction extractSubtypeAction = new CodeAction("Extract as Supertype"); + extractSubtypeAction.setKind(CodeActionKind.RefactorExtract); + actions.add(Either.forRight(extractSubtypeAction)); + WorkspaceEdit extractEdit = new WorkspaceEdit(); + TextEdit newTypeText = new TextEdit(); + newTypeText.setNewText(".type " + "subType <: " + currentSymbol + "\n\n"); + + int line = currentSymbol.getRange().getStart().getLine() + 1; + + List subtypeEdits = new ArrayList(); + if (currentSymbol.getKind() != SouffleSymbolType.TYPE_DECL) { + TextEdit replaceTypeEdit = new TextEdit(); + replaceTypeEdit.setNewText("subType"); + replaceTypeEdit.setRange(currentSymbol.getRange()); + subtypeEdits.add(replaceTypeEdit); + line = currentSymbol.getRange().getStart().getLine() - 1; + } + extractSubtypeAction.setData("subType"); + Position start = new Position(line, 0); + + Range newTypeRange = new Range(start, start); + newTypeText.setRange(newTypeRange); + + subtypeEdits.add(newTypeText); + extractEdit.setChanges(Map.of(params.getTextDocument().getUri(), subtypeEdits)); + extractSubtypeAction.setEdit(extractEdit); + + CodeAction extractTypeAction = new CodeAction("Extract Type"); + extractTypeAction.setKind(CodeActionKind.RefactorExtract); + actions.add(Either.forRight(extractTypeAction)); + WorkspaceEdit extractTypeEdit = new WorkspaceEdit(); + TextEdit newTypeText1 = new TextEdit(); + newTypeText1.setNewText(".type " + "extractedType = " + currentSymbol + "\n\n"); + + newTypeText1.setRange(newTypeRange); + List extractEdits = new ArrayList(); + extractEdits.add(newTypeText1); + if (currentSymbol.getKind() != SouffleSymbolType.TYPE_DECL) { + TextEdit replaceTypeEdit1 = new TextEdit(); + replaceTypeEdit1.setNewText("extractedType"); + replaceTypeEdit1.setRange(currentSymbol.getRange()); + extractEdits.add(replaceTypeEdit1); + } + extractTypeEdit.setChanges(Map.of(params.getTextDocument().getUri(), extractEdits)); + extractTypeAction.setEdit(extractTypeEdit); + } + + private void generateIOCodeAction(CodeActionParams params, List> actions, SouffleContext context, SouffleSymbol currentSymbol) { + CodeAction inputAction = new CodeAction("Generate .input for relation " + currentSymbol.getName()); + inputAction.setKind(CodeActionKind.Refactor); + actions.add(Either.forRight(inputAction)); + WorkspaceEdit edit = new WorkspaceEdit(); + TextEdit textEdit = new TextEdit(); + textEdit.setNewText("\t.input " + currentSymbol.getName() + "()\n"); + + Position end; + if (currentSymbol.getKind() == SouffleSymbolType.RELATION_DECL) { + end = currentSymbol.getRange().getEnd(); + } else { + end = context.getRange().getEnd(); + } + Position position = new Position(end.getLine() + 2, 0); + Range newRange = new Range(position, position); + textEdit.setRange(newRange); + edit.setChanges(Map.of(params.getTextDocument().getUri(), List.of(textEdit))); + inputAction.setEdit(edit); + + CodeAction outputAction = new CodeAction("Generate .output for relation " + currentSymbol.getName()); + outputAction.setKind(CodeActionKind.Refactor); + actions.add(Either.forRight(outputAction)); + WorkspaceEdit edit1 = new WorkspaceEdit(); + TextEdit textEdit1 = new TextEdit(); + textEdit1.setNewText("\t.output " + currentSymbol.getName() + "()\n"); + + textEdit1.setRange(newRange); + edit1.setChanges(Map.of(params.getTextDocument().getUri(), List.of(textEdit1))); + outputAction.setEdit(edit1); + + if ((currentSymbol.getKind() == SouffleSymbolType.RELATION_DECL || + currentSymbol.getKind() == SouffleSymbolType.COMPONENT_DECL) && + currentSymbol.getPotentialDocumentation().getKey() != null) { + CodeAction formatComments = new CodeAction("Format documentation with /* */"); + formatComments.setKind(CodeActionKind.RefactorRewrite); + WorkspaceEdit commentEdit = new WorkspaceEdit(); + TextEdit commentTextEdit = new TextEdit(); + commentTextEdit.setRange(currentSymbol.getPotentialDocumentation().getValue()); + commentTextEdit.setNewText(currentSymbol.getPotentialDocumentation().getKey()); + commentEdit.setChanges(Map.of(params.getTextDocument().getUri(), List.of(commentTextEdit))); + formatComments.setEdit(commentEdit); + actions.add(Either.forRight(formatComments)); + } + } + + private void lintCodeAction(CodeActionParams params, List> actions) { + CodeAction codeAction = new CodeAction("Lint with souffle-lint"); + Command command = new Command(); + command.setCommand("souffle-lint"); + Path path = Path.of(URI.create(params.getTextDocument().getUri())); + command.setArguments(List.of(path.toString())); + codeAction.setCommand(command); + codeAction.setKind(CodeActionKind.Source + ".lint"); + actions.add(Either.forRight(codeAction)); + } +} \ No newline at end of file diff --git a/src/main/java/LSClientLogger.java b/src/main/java/LSClientLogger.java index 1826e23..12751a0 100644 --- a/src/main/java/LSClientLogger.java +++ b/src/main/java/LSClientLogger.java @@ -77,4 +77,29 @@ public void reportHint(Range range, String uri, String message){ diagnostics.get(uri).add(diagnostic); client.publishDiagnostics(new PublishDiagnosticsParams(uri, diagnostics.get(uri))); } + + public void reportLints(List lints, String uri){ + for (SouffleLint lint: lints){ + if(lint != null){ + for(SouffleLintContext fragment: lint.fragments){ + Diagnostic diagnostic = new Diagnostic(); + diagnostic.setSeverity(DiagnosticSeverity.Warning); + String message = lint.rule.name + ": " + lint.rule.shortDescription + "\n\n"; + message+= "Examples: \n" + lint.rule.getExamples(); + diagnostic.setMessage(message); + Position start = new Position(fragment.start.row, fragment.start.column); + Position end = new Position(fragment.end.row, fragment.end.column); + Range range = new Range(start, end); + diagnostic.setRange(range); + diagnostics.get(uri).add(diagnostic); + } + } + } + if(lints.isEmpty()){ + MessageParams messageParams = new MessageParams(); + messageParams.setMessage("No problems found with souffle-lint"); + client.showMessage(messageParams); + } + client.publishDiagnostics(new PublishDiagnosticsParams(uri, diagnostics.get(uri))); + } } diff --git a/src/main/java/SouffleLanguageServer.java b/src/main/java/SouffleLanguageServer.java index 2fc4574..36a2684 100644 --- a/src/main/java/SouffleLanguageServer.java +++ b/src/main/java/SouffleLanguageServer.java @@ -64,16 +64,20 @@ public CompletableFuture initialize(InitializeParams initializ serverCapabilities.setRenameProvider(true); // serverCapabilities.setCodeActionProvider(true); CodeActionOptions codeActionOptions = new CodeActionOptions(); + codeActionOptions.setResolveProvider(true); codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.Source, CodeActionKind.Empty, CodeActionKind.QuickFix)); serverCapabilities.setCodeActionProvider(codeActionOptions); ExecuteCommandOptions executeCommandOptions = new ExecuteCommandOptions(); - executeCommandOptions.setCommands(List.of("lint")); + executeCommandOptions.setCommands(List.of("souffle-lint", "souffle-lint-all")); serverCapabilities.setExecuteCommandProvider(executeCommandOptions); final InitializeResult response = new InitializeResult(serverCapabilities); //Set the document synchronization capabilities to full. this.clientCapabilities = initializeParams.getCapabilities(); + CodeActionResolveSupportCapabilities codeActionResolveSupportCapabilities = new CodeActionResolveSupportCapabilities(); + codeActionResolveSupportCapabilities.setProperties(List.of("edit")); + this.clientCapabilities.getTextDocument().getCodeAction().setResolveSupport(codeActionResolveSupportCapabilities); /* Check if dynamic registration of completion capability is allowed by the client. If so we don't register the capability. Else, we register the completion capability. */ @@ -86,7 +90,9 @@ public CompletableFuture initialize(InitializeParams initializ projectContext = SouffleProjectContext.getInstance(); List workspaceFolders = initializeParams.getWorkspaceFolders(); if(workspaceFolders != null && !workspaceFolders.isEmpty()){ - traverseWorkspace(URI.create(workspaceFolders.get(0).getUri()).getPath()); + String directory = URI.create(workspaceFolders.get(0).getUri()).getPath(); + projectContext.setProjectPath(directory); + traverseWorkspace(directory); } return CompletableFuture.supplyAsync(() -> response); } diff --git a/src/main/java/SouffleLint.java b/src/main/java/SouffleLint.java new file mode 100644 index 0000000..50bbf6e --- /dev/null +++ b/src/main/java/SouffleLint.java @@ -0,0 +1,8 @@ +import java.util.List; + +public class SouffleLint { + List fragments; + SouffleLintRule rule; + String source; + String source_file; +} diff --git a/src/main/java/SouffleLintContext.java b/src/main/java/SouffleLintContext.java new file mode 100644 index 0000000..5c9c650 --- /dev/null +++ b/src/main/java/SouffleLintContext.java @@ -0,0 +1,7 @@ +public class SouffleLintContext { + String node_text; + SouffleLintContext context; + SouffleLintPoint start; + SouffleLintPoint end; + +} diff --git a/src/main/java/SouffleLintExample.java b/src/main/java/SouffleLintExample.java new file mode 100644 index 0000000..4d4939d --- /dev/null +++ b/src/main/java/SouffleLintExample.java @@ -0,0 +1,4 @@ +public class SouffleLintExample { + String before; + String after; +} diff --git a/src/main/java/SouffleLintPoint.java b/src/main/java/SouffleLintPoint.java new file mode 100644 index 0000000..22326b4 --- /dev/null +++ b/src/main/java/SouffleLintPoint.java @@ -0,0 +1,4 @@ +public class SouffleLintPoint { + int row; + int column; +} diff --git a/src/main/java/SouffleLintRule.java b/src/main/java/SouffleLintRule.java new file mode 100644 index 0000000..ca89512 --- /dev/null +++ b/src/main/java/SouffleLintRule.java @@ -0,0 +1,27 @@ +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class SouffleLintRule { + String name; + @SerializedName("short") + String shortDescription; + @SerializedName("long") + String longDescription; + boolean captures; + boolean slow; + List queries; + List examples; + + + public String getExamples(){ + StringBuilder stringBuilder = new StringBuilder(); + for (SouffleLintExample example: examples){ + stringBuilder.append(example.before); + stringBuilder.append(example.after.trim()); + stringBuilder.append(",\n"); + } + stringBuilder.deleteCharAt(stringBuilder.length() - 2); + return stringBuilder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/SouffleLints.java b/src/main/java/SouffleLints.java new file mode 100644 index 0000000..0d735d9 --- /dev/null +++ b/src/main/java/SouffleLints.java @@ -0,0 +1,5 @@ +import java.util.List; + +public class SouffleLints { + List lints; +} diff --git a/src/main/java/SouffleTextDocumentService.java b/src/main/java/SouffleTextDocumentService.java index 683d3e8..1dbe27e 100644 --- a/src/main/java/SouffleTextDocumentService.java +++ b/src/main/java/SouffleTextDocumentService.java @@ -9,18 +9,13 @@ import parsing.preprocessor.PreprocessorParser; import parsing.souffle.SouffleLexer; import parsing.souffle.SouffleParser; -import parsing.symbols.SouffleContext; -import parsing.symbols.SouffleProjectContext; -import parsing.symbols.SouffleSymbol; -import parsing.symbols.SouffleSymbolType; +import parsing.symbols.*; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -116,6 +111,9 @@ public void didSave(DidSaveTextDocumentParams didSaveTextDocumentParams) { CompletionProvider.state = CompletionState.IDLE; this.clientLogger.logMessage("Operation '" + "text/didSave" + "' {fileUri: '" + didSaveTextDocumentParams.getTextDocument().getUri() + "'} Saved"); + + + } catch (IOException | URISyntaxException e) { e.printStackTrace(); } @@ -130,75 +128,14 @@ public CompletableFuture, List>> codeAction(CodeActionParams params) { + return CompletableFuture.supplyAsync(() -> new CodeActionProvider().getCodeAction(params)); + } + + @Override + public CompletableFuture resolveCodeAction(CodeAction unresolved) { return CompletableFuture.supplyAsync(() -> { - List> actions = new ArrayList<>(); - -// CodeAction codeAction = new CodeAction("Lint with Souffle Lint"); -// Command command = new Command(); -// command.setCommand("lint"); -// Path path = Path.of(URI.create(params.getTextDocument().getUri())); -// command.setArguments(List.of(path.toString())); -// codeAction.setCommand(command); -// codeAction.setKind(CodeActionKind.Source+".lint"); -//// codeAction.setDiagnostics(LSClientLogger.getInstance().diagnostics.get(params.getTextDocument().getUri())); -// actions.add(Either.forRight(codeAction)); - - Range cursor = params.getRange(); - SouffleContext context = SouffleProjectContext.getInstance().getContext(params.getTextDocument().getUri(), cursor); - if (context != null) { - SouffleSymbol currentSymbol = context.getSymbol(cursor); - if (currentSymbol != null) { - if(currentSymbol.getKind() == SouffleSymbolType.RELATION_USE || - currentSymbol.getKind() == SouffleSymbolType.RELATION_DECL - ){ - CodeAction inputAction = new CodeAction("Generate .input for relation " + currentSymbol.getName()); - inputAction.setKind(CodeActionKind.Refactor); - actions.add(Either.forRight(inputAction)); - WorkspaceEdit edit = new WorkspaceEdit(); - TextEdit textEdit = new TextEdit(); - textEdit.setNewText(".input " + currentSymbol.getName() + "()\n"); - - - Position end; - if(currentSymbol.getKind() == SouffleSymbolType.RELATION_DECL){ - end = currentSymbol.getRange().getEnd(); - } else { - end = context.getRange().getEnd(); - } - Position position = new Position(end.getLine() + 2, 0); - Range newRange = new Range(position, position); - textEdit.setRange(newRange); - edit.setChanges(Map.of(params.getTextDocument().getUri(), List.of(textEdit))); - inputAction.setEdit(edit); - - CodeAction outputAction = new CodeAction("Generate .output for relation " + currentSymbol.getName()); - outputAction.setKind(CodeActionKind.Refactor); - actions.add(Either.forRight(outputAction)); - WorkspaceEdit edit1 = new WorkspaceEdit(); - TextEdit textEdit1 = new TextEdit(); - textEdit1.setNewText(".output " + currentSymbol.getName() + "()\n"); - - textEdit1.setRange(newRange); - edit1.setChanges(Map.of(params.getTextDocument().getUri(), List.of(textEdit1))); - outputAction.setEdit(edit1); - - if(currentSymbol.getKind() == SouffleSymbolType.RELATION_DECL && currentSymbol.getPotentialDocumentation().getKey() != null){ - CodeAction formatComments = new CodeAction("Format documentation with /* */"); - formatComments.setKind(CodeActionKind.Refactor); - WorkspaceEdit commentEdit = new WorkspaceEdit(); - TextEdit commentTextEdit = new TextEdit(); - commentTextEdit.setRange(currentSymbol.getPotentialDocumentation().getValue()); - commentTextEdit.setNewText(currentSymbol.getPotentialDocumentation().getKey()); - commentEdit.setChanges(Map.of(params.getTextDocument().getUri(), List.of(commentTextEdit))); - formatComments.setEdit(commentEdit); - actions.add(Either.forRight(formatComments)); - } - - } - } - } - - return actions; + System.err.println(unresolved); + return unresolved; }); } diff --git a/src/main/java/SouffleWorkSpaceService.java b/src/main/java/SouffleWorkSpaceService.java index 595a718..51b2b43 100644 --- a/src/main/java/SouffleWorkSpaceService.java +++ b/src/main/java/SouffleWorkSpaceService.java @@ -1,10 +1,17 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.RenameFilesParams; import org.eclipse.lsp4j.services.WorkspaceService; +import org.eclipse.xtext.xbase.lib.Pair; import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -39,12 +46,22 @@ public void didRenameFiles(RenameFilesParams params) { public CompletableFuture executeCommand(ExecuteCommandParams params) { return CompletableFuture.supplyAsync(() -> { String path = params.getArguments().get(0).toString().replaceAll("\"", ""); - ProcessBuilder processBuilder = new ProcessBuilder("souffle-lint","lint", path); + ProcessBuilder processBuilder = new ProcessBuilder("souffle-lint","lint","--format","json", path); Process p; try { p = processBuilder.start(); p.waitFor(); - return new String(p.getInputStream().readAllBytes()); + String json = new String(p.getInputStream().readAllBytes()); + json = "[" + json.replaceAll("}\n", "},") + "]"; + + Gson gson = (new GsonBuilder()).serializeNulls().create(); + SouffleLint[] souffleLints = gson.fromJson(json, SouffleLint[].class); + List lints = Arrays.asList(souffleLints); + LSClientLogger.getInstance().reportLints(lints, Path.of(path).toUri().toString()); + + System.err.println(lints); + + return null; } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } diff --git a/src/main/java/parsing/Utils.java b/src/main/java/parsing/Utils.java index 87565ff..525b42d 100644 --- a/src/main/java/parsing/Utils.java +++ b/src/main/java/parsing/Utils.java @@ -52,18 +52,19 @@ public static Pair getPotentialDocumentation(Token ctx, SoufflePa BufferedTokenStream tokens = (BufferedTokenStream) souffleParser.getTokenStream(); List cmtChannel = tokens.getHiddenTokensToLeft(i, SouffleLexer.HIDDEN); - StringBuilder sb = new StringBuilder("/**\n"); + StringBuilder sb = new StringBuilder("\t/**\n"); if ( cmtChannel!=null ) { for(Token token: cmtChannel){ + if(token != null && token.getText().contains("//")){ sb.append("\t\t"); sb.append(token.getText().replaceAll("\\*", "").replaceAll("/", "").trim()); sb.append("\n"); } else return Pair.of(null,null); } - sb.append("*/"); + sb.append("\t*/"); documentation = sb.toString(); - Position p1 = new Position(cmtChannel.get(0).getLine() - 1, cmtChannel.get(0).getCharPositionInLine()); + Position p1 = new Position(cmtChannel.get(0).getLine() - 1, 0); Position p2 = new Position(cmtChannel.get(cmtChannel.size() - 1).getLine() - 1, cmtChannel.get(cmtChannel.size() - 1).getStopIndex()); range = new Range(p1, p2); } diff --git a/src/main/java/parsing/symbols/SouffleProjectContext.java b/src/main/java/parsing/symbols/SouffleProjectContext.java index 1dc90bd..66815dd 100644 --- a/src/main/java/parsing/symbols/SouffleProjectContext.java +++ b/src/main/java/parsing/symbols/SouffleProjectContext.java @@ -13,6 +13,8 @@ public Map getDocuments() { } private final Map documents; + + private String projectPath; public Set defines; private Range cursorPosition; @@ -78,6 +80,14 @@ public void addDocument(String documentUri, SouffleContext souffleContext){ documents.put(documentUri, souffleContext); } + public String getProjectPath() { + return projectPath; + } + + public void setProjectPath(String projectPath) { + this.projectPath = projectPath; + } + public Range getCursorPosition() { return cursorPosition; }