diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java index 28d8ca71f4c003bd65e1fa5b3eadb2a81410231f..b9422585d7d610489bec3910bc75eb45057a9ba4 100644 --- a/src/main/java/de/prob2/jupyter/ProBKernel.java +++ b/src/main/java/de/prob2/jupyter/ProBKernel.java @@ -23,6 +23,7 @@ import de.prob2.jupyter.commands.AssertCommand; import de.prob2.jupyter.commands.BrowseCommand; import de.prob2.jupyter.commands.Command; import de.prob2.jupyter.commands.CommandExecutionException; +import de.prob2.jupyter.commands.CommandUtils; import de.prob2.jupyter.commands.ConstantsCommand; import de.prob2.jupyter.commands.EvalCommand; import de.prob2.jupyter.commands.ExecCommand; @@ -240,11 +241,7 @@ public final class ProBKernel extends BaseKernel { final String argString = commandMatcher.group(2) == null ? "" : commandMatcher.group(2); if (this.getCommands().containsKey(name)) { final ReplacementOptions replacements = this.getCommands().get(name).complete(this, argString, at - argOffset); - return replacements == null ? null : new ReplacementOptions( - replacements.getReplacements(), - replacements.getSourceStart() + argOffset, - replacements.getSourceEnd() + argOffset - ); + return replacements == null ? null : CommandUtils.offsetReplacementOptions(replacements, argOffset); } else { // Invalid command, can't provide any completions. return null; diff --git a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java index d78e3ab46255d1d293eddf9defd64ea1f110e0e5..7c311174aab0b8c92f51ff5473f2f561dea3ba8b 100644 --- a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java @@ -10,6 +10,7 @@ import de.prob.statespace.AnimationSelector; import de.prob2.jupyter.ProBKernel; import de.prob2.jupyter.UserErrorException; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import io.github.spencerpark.jupyter.kernel.display.mime.MIMEType; @@ -51,4 +52,9 @@ public final class AssertCommand implements Command { } throw new UserErrorException("Assertion is not true: " + displayData.getData(MIMEType.TEXT_PLAIN)); } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/CommandUtils.java b/src/main/java/de/prob2/jupyter/commands/CommandUtils.java index ab57f2059bb07d98db9a8e43dd9c0d224e4eddc8..4aa1ffc979496833d622536a0779acb1b3a0c791 100644 --- a/src/main/java/de/prob2/jupyter/commands/CommandUtils.java +++ b/src/main/java/de/prob2/jupyter/commands/CommandUtils.java @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; public final class CommandUtils { private static final @NotNull Logger LOGGER = LoggerFactory.getLogger(CommandUtils.class); + public static final @NotNull Pattern ARG_SPLIT_PATTERN = Pattern.compile("\\h+"); private static final @NotNull Pattern B_IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]*"); private CommandUtils() { @@ -41,8 +42,34 @@ public final class CommandUtils { throw new AssertionError("Utility class"); } + public static @NotNull String prettyOperationName(final @NotNull String name) { + switch (name) { + case "$setup_constants": + return "SETUP_CONSTANTS"; + + case "$initialise_machine": + return "INITIALISATION"; + + default: + return name; + } + } + + public static @NotNull String unprettyOperationName(final @NotNull String name) { + switch (name) { + case "SETUP_CONSTANTS": + return "$setup_constants"; + + case "INITIALISATION": + return "$initialise_machine"; + + default: + return name; + } + } + public static @NotNull List<@NotNull String> splitArgs(final @NotNull String args, final int limit) { - final String[] split = args.split("\\h+", limit); + final String[] split = ARG_SPLIT_PATTERN.split(args, limit); if (split.length == 1 && split[0].isEmpty()) { return Collections.emptyList(); } else { @@ -127,6 +154,14 @@ public final class CommandUtils { } } + public static @NotNull ReplacementOptions offsetReplacementOptions(final @NotNull ReplacementOptions replacements, final int offset) { + return new ReplacementOptions( + replacements.getReplacements(), + replacements.getSourceStart() + offset, + replacements.getSourceEnd() + offset + ); + } + public static @NotNull ReplacementOptions completeInBExpression(final @NotNull Trace trace, final @NotNull String code, final int at) { final Matcher identifierMatcher = B_IDENTIFIER_PATTERN.matcher(code); String identifier = ""; diff --git a/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java b/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java index d12caaef450ff3a0f79b3df2ebd11a10f5e7973d..2535c0dd616df4c885edbf8952c9f775c32fbca0 100644 --- a/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java @@ -13,6 +13,7 @@ import de.prob.statespace.Transition; import de.prob2.jupyter.ProBKernel; import de.prob2.jupyter.UserErrorException; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -55,4 +56,9 @@ public final class ConstantsCommand implements Command { trace.getStateSpace().evaluateTransitions(Collections.singleton(op), FormulaExpand.TRUNCATE); return new DisplayData(String.format("Machine constants set up using operation %s: %s", op.getId(), op.getRep())); } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/ExecCommand.java b/src/main/java/de/prob2/jupyter/commands/ExecCommand.java index 286c4e96421fe17f731d6f93b42846f131310240..31774a1e89d4e48f5331e0c2052c4322d8e65f13 100644 --- a/src/main/java/de/prob2/jupyter/commands/ExecCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/ExecCommand.java @@ -3,6 +3,9 @@ package de.prob2.jupyter.commands; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.inject.Inject; @@ -14,6 +17,7 @@ import de.prob.statespace.Transition; import de.prob2.jupyter.ProBKernel; import de.prob2.jupyter.UserErrorException; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -56,14 +60,7 @@ public final class ExecCommand implements Command { op = opt.get(); } else { // Transition not found, assume that the argument is an operation name instead. - final String translatedOpName; - if ("SETUP_CONSTANTS".equals(opNameOrId)) { - translatedOpName = "$setup_constants"; - } else if ("INITIALISATION".equals(opNameOrId)) { - translatedOpName = "$initialise_machine"; - } else { - translatedOpName = opNameOrId; - } + final String translatedOpName = CommandUtils.unprettyOperationName(opNameOrId); final List<String> predicates = split.size() < 2 ? Collections.emptyList() : Collections.singletonList(split.get(1)); op = trace.getCurrentState().findTransition(translatedOpName, predicates); if (op == null) { @@ -75,4 +72,33 @@ public final class ExecCommand implements Command { trace.getStateSpace().evaluateTransitions(Collections.singleton(op), FormulaExpand.TRUNCATE); return new DisplayData(String.format("Executed operation %s: %s", op.getId(), op.getRep())); } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + final int opNameEnd; + final Matcher argSplitMatcher = CommandUtils.ARG_SPLIT_PATTERN.matcher(argString); + if (argSplitMatcher.find()) { + opNameEnd = argSplitMatcher.start(); + } else { + opNameEnd = argString.length(); + } + + if (opNameEnd < at) { + // Cursor is in the predicate part of the arguments, provide B completions. + final ReplacementOptions replacements = CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString.substring(opNameEnd), at - opNameEnd); + return CommandUtils.offsetReplacementOptions(replacements, opNameEnd); + } else { + // Cursor is in the first part of the arguments, provide possible operation names and transition IDs. + final String prefix = argString.substring(0, at); + final List<String> opNames = this.animationSelector.getCurrentTrace() + .getNextTransitions() + .stream() + .flatMap(t -> Stream.of(CommandUtils.prettyOperationName(t.getName()), t.getId())) + .distinct() + .filter(s -> s.startsWith(prefix)) + .sorted() + .collect(Collectors.toList()); + return new ReplacementOptions(opNames, 0, opNameEnd); + } + } } diff --git a/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java b/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java index 52fe6e734b968dfcdc93ebf22cbac7b144746a47..b2e94e5d05ac1862e7410e5c03e05b9910059192 100644 --- a/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java @@ -13,6 +13,7 @@ import de.prob.statespace.Transition; import de.prob2.jupyter.ProBKernel; import de.prob2.jupyter.UserErrorException; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -55,4 +56,9 @@ public final class InitialiseCommand implements Command { trace.getStateSpace().evaluateTransitions(Collections.singleton(op), FormulaExpand.TRUNCATE); return new DisplayData(String.format("Machine initialised using operation %s: %s", op.getId(), op.getRep())); } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java b/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java index e57886376cf02abc3c1d812e663f47ef1aa4992c..6d5a6442c454c4d0c40c205a119c63a8f6938adc 100644 --- a/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java @@ -9,6 +9,7 @@ import de.prob.statespace.AnimationSelector; import de.prob2.jupyter.ProBKernel; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -47,4 +48,9 @@ public final class PrettyPrintCommand implements Command { ret.putLatex('$' + cmdLatex.getPrettyPrint() + '$'); return ret; } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java index 174f052b5ae1fbb69cc4ef246156072e6e2bddd8..f4b2d960bc48db89e45f3e661206714c39c9638c 100644 --- a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java @@ -1,6 +1,9 @@ package de.prob2.jupyter.commands; import java.util.List; +import java.util.regex.Matcher; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.inject.Inject; @@ -13,6 +16,7 @@ import de.prob.statespace.Trace; import de.prob2.jupyter.ProBKernel; import de.prob2.jupyter.UserErrorException; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -76,4 +80,29 @@ public final class SolveCommand implements Command { trace.getStateSpace().execute(cmd); return CommandUtils.displayDataForEvalResult(cmd.getValue()); } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + final int solverNameEnd; + final Matcher argSplitMatcher = CommandUtils.ARG_SPLIT_PATTERN.matcher(argString); + if (argSplitMatcher.find()) { + solverNameEnd = argSplitMatcher.start(); + } else { + solverNameEnd = argString.length(); + } + + if (solverNameEnd < at) { + // Cursor is in the predicate part of the arguments, provide B completions. + final ReplacementOptions replacements = CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString.substring(solverNameEnd), at - solverNameEnd); + return CommandUtils.offsetReplacementOptions(replacements, solverNameEnd); + } else { + // Cursor is in the solver name. + final String prefix = argString.substring(0, at); + final List<String> solverNames = Stream.of("prob", "kodkod", "smt_supported_interpreter", "z3", "cvc4") + .filter(s -> s.startsWith(prefix)) + .sorted() + .collect(Collectors.toList()); + return new ReplacementOptions(solverNames, 0, solverNameEnd); + } + } } diff --git a/src/main/java/de/prob2/jupyter/commands/TableCommand.java b/src/main/java/de/prob2/jupyter/commands/TableCommand.java index 6f0c95a5158ec1c46a325b7db4f0e6a552f2d7f3..0d22227c1528240158861493f1bf0aa330da7b18 100644 --- a/src/main/java/de/prob2/jupyter/commands/TableCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/TableCommand.java @@ -16,6 +16,7 @@ import de.prob.statespace.Trace; import de.prob2.jupyter.ProBKernel; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -80,4 +81,9 @@ public final class TableCommand implements Command { res.putMarkdown(sbMarkdown.toString()); return res; } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/TypeCommand.java b/src/main/java/de/prob2/jupyter/commands/TypeCommand.java index 2eb29f8bb11f310bb61d48564bd2ad2f22b4b20d..13167c49805e0b5392f0d47c4789abd7810c584e 100644 --- a/src/main/java/de/prob2/jupyter/commands/TypeCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/TypeCommand.java @@ -11,6 +11,7 @@ import de.prob.statespace.Trace; import de.prob2.jupyter.ProBKernel; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import org.jetbrains.annotations.NotNull; @@ -46,4 +47,9 @@ public final class TypeCommand implements Command { throw new ProBError("Type errors in formula", result.getErrors()); } } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at); + } }