diff --git a/src/org/rascalmpl/library/Messages.java b/src/org/rascalmpl/library/Messages.java index a706600c504..26121a15c28 100644 --- a/src/org/rascalmpl/library/Messages.java +++ b/src/org/rascalmpl/library/Messages.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,6 +44,33 @@ public class Messages { private static final io.usethesource.vallang.type.Type Message_warning = tf.constructor(ts, Message, "warning", tf.stringType(), "msg", tf.sourceLocationType(), "at"); private static final io.usethesource.vallang.type.Type Message_error = tf.constructor(ts, Message, "error", tf.stringType(), "msg", tf.sourceLocationType(), "at"); + // These declarations mirror the data definitions in the `util::IDEServices` module of the standard library + private static final io.usethesource.vallang.type.Type CodeAction = tf.abstractDataType(ts, "CodeAction"); + public static final io.usethesource.vallang.type.Type CodeAction_action = tf.constructor(ts, CodeAction, "action"); + + public static final io.usethesource.vallang.type.Type Command = tf.abstractDataType(ts, "Command"); + private static final io.usethesource.vallang.type.Type Command_noop = tf.constructor(ts, Command, "noop"); + + // These declarations mirror the data definition in the `analysis::diff::edits::FileSystemChanges` module of the standard library + private static final io.usethesource.vallang.type.Type FileSystemChange = tf.abstractDataType(ts, "FileSystemChange"); + private static final io.usethesource.vallang.type.Type FileSystemChange_removed = tf.constructor(ts, FileSystemChange, "removed", tf.sourceLocationType(), "file"); + private static final io.usethesource.vallang.type.Type FileSystemChange_created = tf.constructor(ts, FileSystemChange, "created", tf.sourceLocationType(), "file"); + private static final io.usethesource.vallang.type.Type FileSystemChange_renamed = tf.constructor(ts, FileSystemChange, "renamed", tf.sourceLocationType(), "from", tf.sourceLocationType(), "to"); + private static final io.usethesource.vallang.type.Type FileSystemChange_changed = tf.constructor(ts, FileSystemChange, "changed", tf.sourceLocationType(), "file"); + + static { + // data Message(list[CodeAction] fixes = []]); + ts.declareKeywordParameter(Message, "fixes", tf.listType(CodeAction)); + + // data CodeAction(list[FileSystemChange] edits = [], Command command = noop(), str title = command.title); + ts.declareKeywordParameter(CodeAction, "edits", tf.listType(FileSystemChange)); + ts.declareKeywordParameter(CodeAction, "command", Command); + ts.declareKeywordParameter(CodeAction, "title", tf.stringType()); + + // data Command(str title=""); + ts.declareKeywordParameter(CodeAction, "title", tf.stringType()); + } + public static IConstructor info(String message, ISourceLocation loc) { return message(Message_info, message, loc); } @@ -73,6 +101,18 @@ public static IConstructor addCause(IConstructor msg, String cause, ISourceLocat return msg.asWithKeywordParameters().setParameter("causes", causes); } + public static IConstructor addFix(IConstructor msg, IConstructor fix) { + if (fix == null) { + return msg; + } + + var kw = msg.asWithKeywordParameters(); + IList fixes = (IList) kw.getParameter("fixes"); + fixes = fixes == null ? vf.list(fix) : fixes.append(fix); + + return kw.setParameter("fixes", fixes); + } + public static boolean isError(IValue v) { return v instanceof IConstructor && ((IConstructor) v).getConstructorType() == Message_error; } diff --git a/src/org/rascalmpl/library/util/PathConfig.java b/src/org/rascalmpl/library/util/PathConfig.java index 2c94283f391..359d9cb8013 100644 --- a/src/org/rascalmpl/library/util/PathConfig.java +++ b/src/org/rascalmpl/library/util/PathConfig.java @@ -520,7 +520,7 @@ private static void buildRascalSelfApplicationConfig(ISourceLocation workspaceRa * Configure paths for the rascal-lsp project when it's open in the IDE (the runtime inside rascal-lsp is not configured here) */ private static void buildRascalLSPConfig(ISourceLocation manifestRoot, RascalConfigMode mode, List mavenClasspath, IListWriter srcs, IListWriter libs, IListWriter messages) throws IOException { - var rascalRuntime = getRelevantRascal(mavenClasspath, mode); + var rascalRuntime = getRelevantRascal(mavenClasspath, mode, manifestRoot, messages); var insideRascalJar = JarURIResolver.jarify(MavenRepositoryURIResolver.mavenize(rascalRuntime)); var rascalLibrary = URIUtil.getChildLocation(insideRascalJar, "org/rascalmpl/library"); var rascalCompiler = URIUtil.getChildLocation(insideRascalJar, "org/rascalmpl/compiler"); @@ -576,7 +576,7 @@ public static boolean isRascalArtifact(Artifact art) { && coord.getGroupId().equals("org.rascalmpl"); } - private static ISourceLocation getRelevantRascal(List mavenClasspath, RascalConfigMode mode) throws IOException { + private static ISourceLocation getRelevantRascal(List mavenClasspath, RascalConfigMode mode, ISourceLocation manifestRoot, IListWriter messages) throws IOException { if (mode == RascalConfigMode.INTERPRETER_EXTERNAL || mode == RascalConfigMode.COMPILER) { var rascalFromPom = mavenClasspath.stream() .filter(PathConfig::isRascalArtifact) @@ -586,14 +586,34 @@ private static ISourceLocation getRelevantRascal(List mavenClasspath, .map(vf::sourceLocation); if (rascalFromPom.isPresent()) { return rascalFromPom.get(); + } else { + messages.append(makeMissingRascalMessage(manifestRoot)); } } return resolveCurrentRascalRuntime(); } + // This declaration does not mirror a constructor from the standard library; it is here to be able to send the command, but the declaration must live in the implementing project (e.g., `rascal-lsp`) + private static final io.usethesource.vallang.type.Type Command_addRascalDependencyToPom = tf.constructor(store, Messages.Command, "addRascalDependencyToPom", tf.sourceLocationType(), "pomLoc"); + + private static IConstructor addAddRascalDependencyFix(IConstructor msg, ISourceLocation pomXml) { + var title = "Add Rascal dependency to pom.xml"; + var codeAction = vf.constructor(Command_addRascalDependencyToPom, new IValue[] { pomXml }, Map.of("title", vf.string(title))); + var fix = vf.constructor(Messages.CodeAction_action, new IValue[]{}, Map.of("command", codeAction)); + return Messages.addFix(msg, fix); + } + + private static IConstructor makeMissingRascalMessage(ISourceLocation manifestRoot) { + // The `pomXml` location is given an artificial range on the second line to make sure that the hover remains within the bounds of the editor + // Otherwise, the quick-fix pop-up immediately disappears as the editor loses focus + var pomXml = vf.sourceLocation(URIUtil.getChildLocation(manifestRoot, "pom.xml"), 0, 0, 2, 2, 0, 8); + var msg = Messages.warning("Missing required Rascal dependency in project " + URIUtil.getLocationName(manifestRoot), pomXml); + return addAddRascalDependencyFix(msg, pomXml); + } + private static void buildNormalProjectConfig(ISourceLocation manifestRoot, RascalConfigMode mode, List mavenClasspath, boolean isRoot, IListWriter srcs, IListWriter libs, IListWriter messages) throws IOException, URISyntaxException { if (isRoot) { - var rascal = getRelevantRascal(mavenClasspath, mode); + var rascal = getRelevantRascal(mavenClasspath, mode, manifestRoot, messages); var insideRascal = JarURIResolver.jarify(MavenRepositoryURIResolver.mavenize(rascal)); libs.append(rascal);