From e2b97144bbb22ccb95e33f284a2e45400c5f8a84 Mon Sep 17 00:00:00 2001
From: dgelessus <dgelessus@users.noreply.github.com>
Date: Wed, 20 Jun 2018 19:14:04 +0200
Subject: [PATCH] Add code completion support to most commands

Still missing are ::load and :pref.

::render, :groovy and :load are also missing, but implementing code
completion there is probably not worth the effort.
---
 .../java/de/prob2/jupyter/ProBKernel.java     |  7 +---
 .../prob2/jupyter/commands/AssertCommand.java |  6 +++
 .../prob2/jupyter/commands/CommandUtils.java  | 37 +++++++++++++++-
 .../jupyter/commands/ConstantsCommand.java    |  6 +++
 .../prob2/jupyter/commands/ExecCommand.java   | 42 +++++++++++++++----
 .../jupyter/commands/InitialiseCommand.java   |  6 +++
 .../jupyter/commands/PrettyPrintCommand.java  |  6 +++
 .../prob2/jupyter/commands/SolveCommand.java  | 29 +++++++++++++
 .../prob2/jupyter/commands/TableCommand.java  |  6 +++
 .../prob2/jupyter/commands/TypeCommand.java   |  6 +++
 10 files changed, 137 insertions(+), 14 deletions(-)

diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index 28d8ca7..b942258 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 d78e3ab..7c31117 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 ab57f20..4aa1ffc 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 d12caae..2535c0d 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 286c4e9..31774a1 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 52fe6e7..b2e94e5 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 e578863..6d5a644 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 174f052..f4b2d96 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 6f0c95a..0d22227 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 2eb29f8..13167c4 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);
+	}
 }
-- 
GitLab