From cdcfd7555094ec79cc75af600d16cf39a02a0cf4 Mon Sep 17 00:00:00 2001
From: dgelessus <dgelessus@users.noreply.github.com>
Date: Wed, 3 Jun 2020 20:06:46 +0200
Subject: [PATCH] Re-implement command completion feature based on new argument
 parsing

This new implementation replaces the old separate implementation in
CommandUtils.completeArgs. This change is analogous to the previous
change that re-implemented command inspection.
---
 src/main/java/de/prob2/jupyter/Command.java   |  3 +-
 .../java/de/prob2/jupyter/CommandUtils.java   | 28 -------------
 .../de/prob2/jupyter/ParameterCompleters.java | 41 +++++++++++++++++++
 .../java/de/prob2/jupyter/ProBKernel.java     | 34 +++++++++++++--
 .../prob2/jupyter/commands/AssertCommand.java |  8 ++--
 .../prob2/jupyter/commands/BrowseCommand.java |  7 ++--
 .../prob2/jupyter/commands/BsymbCommand.java  |  7 ++--
 .../prob2/jupyter/commands/CheckCommand.java  | 17 +++++---
 .../jupyter/commands/ConstantsCommand.java    |  8 ++--
 .../de/prob2/jupyter/commands/DotCommand.java | 15 ++++---
 .../prob2/jupyter/commands/EvalCommand.java   |  8 ++--
 .../prob2/jupyter/commands/ExecCommand.java   | 15 ++++---
 .../prob2/jupyter/commands/FindCommand.java   |  8 ++--
 .../prob2/jupyter/commands/GotoCommand.java   |  7 ++--
 .../prob2/jupyter/commands/GroovyCommand.java |  7 ++--
 .../prob2/jupyter/commands/HelpCommand.java   | 31 ++++++++------
 .../jupyter/commands/InitialiseCommand.java   |  8 ++--
 .../de/prob2/jupyter/commands/LetCommand.java |  7 ++--
 .../jupyter/commands/LoadCellCommand.java     | 17 +++-----
 .../jupyter/commands/LoadFileCommand.java     | 15 ++++---
 .../jupyter/commands/ModelCheckCommand.java   |  6 +--
 .../prob2/jupyter/commands/PrefCommand.java   |  9 ++--
 .../jupyter/commands/PrettyPrintCommand.java  |  8 ++--
 .../prob2/jupyter/commands/RenderCommand.java |  7 ++--
 .../prob2/jupyter/commands/ShowCommand.java   |  7 ++--
 .../prob2/jupyter/commands/SolveCommand.java  | 15 ++++---
 .../prob2/jupyter/commands/StatsCommand.java  |  7 ++--
 .../prob2/jupyter/commands/TableCommand.java  |  8 ++--
 .../prob2/jupyter/commands/TimeCommand.java   |  8 ++--
 .../prob2/jupyter/commands/TraceCommand.java  |  7 ++--
 .../prob2/jupyter/commands/TypeCommand.java   |  8 ++--
 .../prob2/jupyter/commands/UnletCommand.java  |  6 +--
 .../jupyter/commands/VersionCommand.java      |  7 ++--
 33 files changed, 221 insertions(+), 173 deletions(-)
 create mode 100644 src/main/java/de/prob2/jupyter/ParameterCompleters.java

diff --git a/src/main/java/de/prob2/jupyter/Command.java b/src/main/java/de/prob2/jupyter/Command.java
index 01fccee..bd7b627 100644
--- a/src/main/java/de/prob2/jupyter/Command.java
+++ b/src/main/java/de/prob2/jupyter/Command.java
@@ -1,6 +1,5 @@
 package de.prob2.jupyter;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
@@ -45,5 +44,5 @@ public interface Command {
 	
 	public abstract @NotNull ParameterInspectors getParameterInspectors();
 	
-	public abstract @Nullable ReplacementOptions complete(final @NotNull String argString, final int at);
+	public abstract @NotNull ParameterCompleters getParameterCompleters();
 }
diff --git a/src/main/java/de/prob2/jupyter/CommandUtils.java b/src/main/java/de/prob2/jupyter/CommandUtils.java
index c36d84e..cd77b51 100644
--- a/src/main/java/de/prob2/jupyter/CommandUtils.java
+++ b/src/main/java/de/prob2/jupyter/CommandUtils.java
@@ -300,34 +300,6 @@ 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 @Nullable ReplacementOptions completeArgs(final @NotNull String argString, final int at, final @NotNull Completer @NotNull... completers) {
-		final Matcher argSplitMatcher = ARG_SPLIT_PATTERN.matcher(argString);
-		int argStart = 0;
-		int argEnd = argString.length();
-		int i = 0;
-		while (argSplitMatcher.find()) {
-			if (argSplitMatcher.end() > at) {
-				argEnd = argSplitMatcher.start();
-				break;
-			}
-			argStart = argSplitMatcher.end();
-			if (i >= completers.length-1) {
-				break;
-			}
-			i++;
-		}
-		final ReplacementOptions replacements = completers[i].complete(argString.substring(argStart, argEnd), at - argStart);
-		return replacements == null ? null : offsetReplacementOptions(replacements, argStart);
-	}
-	
 	public static @Nullable Matcher matchBIdentifierAt(final @NotNull String code, final int at) {
 		final Matcher identifierMatcher = B_IDENTIFIER_PATTERN.matcher(code);
 		while (identifierMatcher.find() && identifierMatcher.start() < at) {
diff --git a/src/main/java/de/prob2/jupyter/ParameterCompleters.java b/src/main/java/de/prob2/jupyter/ParameterCompleters.java
new file mode 100644
index 0000000..98e98bd
--- /dev/null
+++ b/src/main/java/de/prob2/jupyter/ParameterCompleters.java
@@ -0,0 +1,41 @@
+package de.prob2.jupyter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.google.common.base.MoreObjects;
+
+import org.jetbrains.annotations.NotNull;
+
+public final class ParameterCompleters {
+	public static final @NotNull ParameterCompleters NONE = new ParameterCompleters(Collections.emptyMap());
+	
+	private final @NotNull Map<@NotNull Parameter<?>, CommandUtils.@NotNull Completer> completers;
+	
+	public ParameterCompleters(final @NotNull Map<@NotNull Parameter<?>, CommandUtils.@NotNull Completer> completers) {
+		super();
+		
+		this.completers = Collections.unmodifiableMap(new HashMap<>(completers));
+	}
+	
+	public @NotNull Map<@NotNull Parameter<?>, CommandUtils.@NotNull Completer> getCompleters() {
+		return this.completers;
+	}
+	
+	@Override
+	public String toString() {
+		return MoreObjects.toStringHelper(this)
+			.add("completers", this.getCompleters())
+			.toString();
+	}
+	
+	public @NotNull Optional<CommandUtils.Completer> getCompleterForParameter(final @NotNull Parameter<?> parameter) {
+		if (this.getCompleters().containsKey(parameter)) {
+			return Optional.of(this.getCompleters().get(parameter));
+		} else {
+			return Optional.empty();
+		}
+	}
+}
diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index b2c9864..7e65456 100644
--- a/src/main/java/de/prob2/jupyter/ProBKernel.java
+++ b/src/main/java/de/prob2/jupyter/ProBKernel.java
@@ -403,6 +403,32 @@ public final class ProBKernel extends BaseKernel {
 		}
 	}
 	
+	private static @NotNull ReplacementOptions offsetReplacementOptions(final @NotNull ReplacementOptions replacements, final int offset) {
+		return new ReplacementOptions(
+			replacements.getReplacements(),
+			replacements.getSourceStart() + offset,
+			replacements.getSourceEnd() + offset
+		);
+	}
+	
+	private static @Nullable ReplacementOptions completeCommandArguments(final @NotNull Command command, final @NotNull String argString, final int at) {
+		final SplitResult split = CommandUtils.splitArgs(command.getParameters(), argString, at);
+		if (split.getParameterAtPosition().isPresent()) {
+			final Optional<CommandUtils.Completer> completer = command.getParameterCompleters().getCompleterForParameter(split.getParameterAtPosition().get());
+			if (completer.isPresent()) {
+				final List<PositionedString> argsAtPosition = split.getArguments().get(split.getParameterAtPosition().get());
+				assert !argsAtPosition.isEmpty();
+				final PositionedString lastArgument = argsAtPosition.get(argsAtPosition.size() - 1);
+				final ReplacementOptions replacements = completer.get().complete(lastArgument.getValue(), at - lastArgument.getStartPosition());
+				return replacements == null ? null : offsetReplacementOptions(replacements, lastArgument.getStartPosition());
+			} else {
+				return null;
+			}
+		} else {
+			return null;
+		}
+	}
+	
 	@Override
 	public @Nullable ReplacementOptions complete(final @NotNull String code, final int at) {
 		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code);
@@ -426,8 +452,8 @@ public final class ProBKernel extends BaseKernel {
 				assert name != null;
 				final String argString = commandMatcher.group(2) == null ? "" : commandMatcher.group(2);
 				if (this.getCommands().containsKey(name)) {
-					final ReplacementOptions replacements = this.getCommands().get(name).complete(argString, at - argOffset);
-					return replacements == null ? null : CommandUtils.offsetReplacementOptions(replacements, argOffset);
+					final ReplacementOptions replacements = completeCommandArguments(this.getCommands().get(name), argString, at - argOffset);
+					return replacements == null ? null : offsetReplacementOptions(replacements, argOffset);
 				} else {
 					// Invalid command, can't provide any completions.
 					return null;
@@ -436,7 +462,7 @@ public final class ProBKernel extends BaseKernel {
 		} else if (SPACE_PATTERN.matcher(code).matches()) {
 			// The code contains only whitespace, provide completions from :eval and for command names.
 			final List<String> replacementStrings = new ArrayList<>();
-			final ReplacementOptions evalReplacements = this.getCommands().get(":eval").complete(code, at);
+			final ReplacementOptions evalReplacements = completeCommandArguments(this.getCommands().get(":eval"), code, at);
 			if (evalReplacements != null) {
 				replacementStrings.addAll(evalReplacements.getReplacements());
 			}
@@ -444,7 +470,7 @@ public final class ProBKernel extends BaseKernel {
 			return new ReplacementOptions(replacementStrings, at, at);
 		} else {
 			// The code is not a valid command, ask :eval for completions.
-			return this.getCommands().get(":eval").complete(code, at);
+			return completeCommandArguments(this.getCommands().get(":eval"), code, at);
 		}
 	}
 	
diff --git a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
index cfe9e22..9b8a8be 100644
--- a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
@@ -12,13 +12,13 @@ import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
 
@@ -91,7 +91,9 @@ public final class AssertCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			FORMULA_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/BrowseCommand.java b/src/main/java/de/prob2/jupyter/commands/BrowseCommand.java
index a42e3c7..36d89b6 100644
--- a/src/main/java/de/prob2/jupyter/commands/BrowseCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/BrowseCommand.java
@@ -12,15 +12,14 @@ import de.prob.statespace.LoadedMachine;
 import de.prob.statespace.Trace;
 import de.prob.statespace.Transition;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 import se.sawano.java.text.AlphanumericComparator;
 
@@ -96,7 +95,7 @@ public final class BrowseCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/BsymbCommand.java b/src/main/java/de/prob2/jupyter/commands/BsymbCommand.java
index fd6b68e..d725b47 100644
--- a/src/main/java/de/prob2/jupyter/commands/BsymbCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/BsymbCommand.java
@@ -3,15 +3,14 @@ package de.prob2.jupyter.commands;
 import com.google.inject.Inject;
 
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class BsymbCommand implements Command {
 	@Inject
@@ -59,7 +58,7 @@ public final class BsymbCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/CheckCommand.java b/src/main/java/de/prob2/jupyter/commands/CheckCommand.java
index 6f8be9f..5dfeac2 100644
--- a/src/main/java/de/prob2/jupyter/commands/CheckCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/CheckCommand.java
@@ -23,6 +23,7 @@ import de.prob.unicode.UnicodeTranslator;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -118,11 +119,15 @@ public final class CheckCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		final String prefix = argString.substring(0, at);
-		return new ReplacementOptions(CHILDREN_BASE_CLASS_MAP.keySet()
-			.stream()
-			.filter(s -> s.startsWith(prefix))
-			.collect(Collectors.toList()), 0, argString.length());
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			WHAT_PARAM, (argString, at) -> {
+				final String prefix = argString.substring(0, at);
+				return new ReplacementOptions(CHILDREN_BASE_CLASS_MAP.keySet()
+					.stream()
+					.filter(s -> s.startsWith(prefix))
+					.collect(Collectors.toList()), 0, argString.length());
+			}
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java b/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java
index d7d2a14..5bb95a3 100644
--- a/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/ConstantsCommand.java
@@ -13,12 +13,12 @@ import de.prob.statespace.Transition;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -87,7 +87,9 @@ public final class ConstantsCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/DotCommand.java b/src/main/java/de/prob2/jupyter/commands/DotCommand.java
index f1d3988..321fa10 100644
--- a/src/main/java/de/prob2/jupyter/commands/DotCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/DotCommand.java
@@ -24,6 +24,7 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -34,7 +35,6 @@ import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class DotCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle COMMAND_PARAM = Parameter.required("command");
@@ -145,14 +145,13 @@ public final class DotCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeArgs(
-			argString, at,
-			(commandName, at0) -> {
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(ImmutableMap.of(
+			COMMAND_PARAM, (commandName, at) -> {
 				final Trace trace = this.animationSelector.getCurrentTrace();
 				final GetAllDotCommands cmd = new GetAllDotCommands(trace.getCurrentState());
 				trace.getStateSpace().execute(cmd);
-				final String prefix = commandName.substring(0, at0);
+				final String prefix = commandName.substring(0, at);
 				final List<String> commands = cmd.getCommands().stream()
 					.filter(DynamicCommandItem::isAvailable)
 					.map(DynamicCommandItem::getCommand)
@@ -161,7 +160,7 @@ public final class DotCommand implements Command {
 					.collect(Collectors.toList());
 				return new ReplacementOptions(commands, 0, commandName.length());
 			},
-			CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
-		);
+			FORMULA_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/EvalCommand.java b/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
index 972926f..ee8e470 100644
--- a/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
@@ -10,12 +10,12 @@ import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -74,7 +74,9 @@ public final class EvalCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			FORMULA_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/ExecCommand.java b/src/main/java/de/prob2/jupyter/commands/ExecCommand.java
index 3b123e2..f391438 100644
--- a/src/main/java/de/prob2/jupyter/commands/ExecCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/ExecCommand.java
@@ -16,6 +16,7 @@ import de.prob.statespace.Transition;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -25,7 +26,6 @@ import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class ExecCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle OPERATION_PARAM = Parameter.required("operation");
@@ -96,11 +96,10 @@ public final class ExecCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeArgs(
-			argString, at,
-			(operation, at0) -> {
-				final String prefix = operation.substring(0, at0);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(ImmutableMap.of(
+			OPERATION_PARAM, (operation, at) -> {
+				final String prefix = operation.substring(0, at);
 				final List<String> opNames = this.animationSelector.getCurrentTrace()
 					.getNextTransitions()
 					.stream()
@@ -112,7 +111,7 @@ public final class ExecCommand implements Command {
 					.collect(Collectors.toList());
 				return new ReplacementOptions(opNames, 0, operation.length());
 			},
-			CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
-		);
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/FindCommand.java b/src/main/java/de/prob2/jupyter/commands/FindCommand.java
index 055d469..8e2beb0 100644
--- a/src/main/java/de/prob2/jupyter/commands/FindCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/FindCommand.java
@@ -12,12 +12,12 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -82,7 +82,9 @@ public final class FindCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/GotoCommand.java b/src/main/java/de/prob2/jupyter/commands/GotoCommand.java
index 557b751..292cc40 100644
--- a/src/main/java/de/prob2/jupyter/commands/GotoCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/GotoCommand.java
@@ -8,16 +8,15 @@ import de.prob.statespace.AnimationSelector;
 import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class GotoCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle INDEX_PARAM = Parameter.required("index");
@@ -74,7 +73,7 @@ public final class GotoCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/GroovyCommand.java b/src/main/java/de/prob2/jupyter/commands/GroovyCommand.java
index 2f085e9..45056ed 100644
--- a/src/main/java/de/prob2/jupyter/commands/GroovyCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/GroovyCommand.java
@@ -12,16 +12,15 @@ import com.google.inject.Injector;
 import de.prob.scripting.ScriptEngineProvider;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class GroovyCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle EXPRESSION_PARAM = Parameter.requiredRemainder("expression");
@@ -82,7 +81,7 @@ public final class GroovyCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
index 9b9ca2f..7b78a10 100644
--- a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
@@ -14,6 +14,7 @@ import com.google.inject.Injector;
 
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -175,18 +176,22 @@ public final class HelpCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		final String prefix = argString.substring(0, at);
-		return new ReplacementOptions(
-			this.injector.getInstance(ProBKernel.class)
-				.getCommands()
-				.keySet()
-				.stream()
-				.filter(s -> s.startsWith(prefix))
-				.sorted()
-				.collect(Collectors.toList()),
-			0,
-			argString.length()
-		);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			COMMAND_NAME_PARAM, (commandName, at) -> {
+				final String prefix = commandName.substring(0, at);
+				return new ReplacementOptions(
+					this.injector.getInstance(ProBKernel.class)
+						.getCommands()
+						.keySet()
+						.stream()
+						.filter(s -> s.startsWith(prefix))
+						.sorted()
+						.collect(Collectors.toList()),
+					0,
+					commandName.length()
+				);
+			}
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java b/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java
index 278de52..27d6426 100644
--- a/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/InitialiseCommand.java
@@ -13,12 +13,12 @@ import de.prob.statespace.Transition;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -87,7 +87,9 @@ public final class InitialiseCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/LetCommand.java b/src/main/java/de/prob2/jupyter/commands/LetCommand.java
index d77fa45..dbe689b 100644
--- a/src/main/java/de/prob2/jupyter/commands/LetCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LetCommand.java
@@ -12,16 +12,15 @@ import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class LetCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle NAME_PARAM = Parameter.required("name");
@@ -83,8 +82,8 @@ public final class LetCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
+	public @NotNull ParameterCompleters getParameterCompleters() {
 		// TODO
-		return null;
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java b/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
index 92a416b..e9dbafd 100644
--- a/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
@@ -14,16 +14,15 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class LoadCellCommand implements Command {
 	private static final @NotNull Parameter.Multiple PREFS_PARAM = Parameter.optionalMultiple("prefs");
@@ -94,14 +93,10 @@ public final class LoadCellCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		final int newlinePos = argString.indexOf('\n');
-		if (newlinePos == -1 || at < newlinePos) {
-			// Cursor is on the first line, provide preference name completions.
-			return CommandUtils.completeInPreferences(this.animationSelector.getCurrentTrace(), argString, at);
-		} else {
-			// Cursor is in the body, provide B completions.
-			return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
-		}
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(ImmutableMap.of(
+			PREFS_PARAM, CommandUtils.preferencesCompleter(this.animationSelector.getCurrentTrace()),
+			CODE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java b/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
index 123d077..48f5e62 100644
--- a/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
@@ -23,6 +23,7 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -33,7 +34,6 @@ import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -120,11 +120,10 @@ public final class LoadFileCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeArgs(
-			argString, at,
-			(filename, at0) -> {
-				final String prefix = filename.substring(0, at0);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(ImmutableMap.of(
+			FILE_NAME_PARAM, (filename, at) -> {
+				final String prefix = filename.substring(0, at);
 				final List<String> fileNames;
 				try (final Stream<Path> list = Files.list(Paths.get(""))) {
 					fileNames = list
@@ -146,7 +145,7 @@ public final class LoadFileCommand implements Command {
 				}
 				return new ReplacementOptions(fileNames, 0, filename.length());
 			},
-			CommandUtils.preferencesCompleter(this.animationSelector.getCurrentTrace())
-		);
+			PREFS_PARAM, CommandUtils.preferencesCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/ModelCheckCommand.java b/src/main/java/de/prob2/jupyter/commands/ModelCheckCommand.java
index 65e1d91..9e2280d 100644
--- a/src/main/java/de/prob2/jupyter/commands/ModelCheckCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/ModelCheckCommand.java
@@ -16,6 +16,7 @@ import de.prob.check.StateSpaceStats;
 import de.prob.statespace.AnimationSelector;
 import de.prob.statespace.StateSpace;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -23,7 +24,6 @@ import de.prob2.jupyter.ProBKernel;
 import de.prob2.jupyter.UserErrorException;
 
 import io.github.spencerpark.jupyter.kernel.JupyterIO;
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
@@ -134,7 +134,7 @@ public final class ModelCheckCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/PrefCommand.java b/src/main/java/de/prob2/jupyter/commands/PrefCommand.java
index da0fe8a..7132c93 100644
--- a/src/main/java/de/prob2/jupyter/commands/PrefCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/PrefCommand.java
@@ -15,16 +15,15 @@ import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class PrefCommand implements Command {
 	private static final @NotNull Parameter.Multiple PREFS_PARAM = Parameter.optionalMultiple("prefs");
@@ -118,7 +117,9 @@ public final class PrefCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInPreferences(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			PREFS_PARAM, CommandUtils.preferencesCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java b/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java
index 0cf7bbb..45b6493 100644
--- a/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/PrettyPrintCommand.java
@@ -11,11 +11,11 @@ import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
@@ -82,7 +82,9 @@ public final class PrettyPrintCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/RenderCommand.java b/src/main/java/de/prob2/jupyter/commands/RenderCommand.java
index 7e1d448..f52c219 100644
--- a/src/main/java/de/prob2/jupyter/commands/RenderCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/RenderCommand.java
@@ -6,15 +6,14 @@ import com.google.inject.Inject;
 
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class RenderCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle MIME_TYPE_PARAM = Parameter.required("mimeType");
@@ -66,7 +65,7 @@ public final class RenderCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/ShowCommand.java b/src/main/java/de/prob2/jupyter/commands/ShowCommand.java
index 9621989..b1ef184 100644
--- a/src/main/java/de/prob2/jupyter/commands/ShowCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/ShowCommand.java
@@ -16,17 +16,16 @@ import de.prob.animator.domainobjects.AnimationMatrixEntry;
 import de.prob.statespace.AnimationSelector;
 import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class ShowCommand implements Command {
 	private final @NotNull AnimationSelector animationSelector;
@@ -158,7 +157,7 @@ public final class ShowCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
index 3b29bc9..8d01043 100644
--- a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
@@ -17,6 +17,7 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
@@ -27,7 +28,6 @@ import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class SolveCommand implements Command {
 	private static final @NotNull Parameter.RequiredSingle SOLVER_PARAM = Parameter.required("solver");
@@ -102,18 +102,17 @@ public final class SolveCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeArgs(
-			argString, at,
-			(solverName, at0) -> {
-				final String prefix = solverName.substring(0, at0);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(ImmutableMap.of(
+			SOLVER_PARAM, (solverName, at) -> {
+				final String prefix = solverName.substring(0, at);
 				final List<String> solverNames = SOLVERS.keySet().stream()
 					.filter(s -> s.startsWith(prefix))
 					.sorted()
 					.collect(Collectors.toList());
 				return new ReplacementOptions(solverNames, 0, solverName.length());
 			},
-			CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
-		);
+			PREDICATE_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/StatsCommand.java b/src/main/java/de/prob2/jupyter/commands/StatsCommand.java
index abe5917..54ffdd4 100644
--- a/src/main/java/de/prob2/jupyter/commands/StatsCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/StatsCommand.java
@@ -6,15 +6,14 @@ import de.prob.animator.command.ComputeStateSpaceStatsCommand;
 import de.prob.check.StateSpaceStats;
 import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class StatsCommand implements Command {
 	private final @NotNull AnimationSelector animationSelector;
@@ -87,7 +86,7 @@ public final class StatsCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/TableCommand.java b/src/main/java/de/prob2/jupyter/commands/TableCommand.java
index 0e09bfa..ea1049f 100644
--- a/src/main/java/de/prob2/jupyter/commands/TableCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/TableCommand.java
@@ -19,12 +19,12 @@ import de.prob.unicode.UnicodeTranslator;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -118,7 +118,9 @@ public final class TableCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			EXPRESSION_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/TimeCommand.java b/src/main/java/de/prob2/jupyter/commands/TimeCommand.java
index 0f653b2..a7cf5c9 100644
--- a/src/main/java/de/prob2/jupyter/commands/TimeCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/TimeCommand.java
@@ -10,12 +10,12 @@ import com.google.inject.Injector;
 
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -81,7 +81,9 @@ public final class TimeCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return this.injector.getInstance(ProBKernel.class).complete(argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			COMMAND_AND_ARGS_PARAM, (argString, at) -> this.injector.getInstance(ProBKernel.class).complete(argString, at)
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/TraceCommand.java b/src/main/java/de/prob2/jupyter/commands/TraceCommand.java
index 6073818..82db34f 100644
--- a/src/main/java/de/prob2/jupyter/commands/TraceCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/TraceCommand.java
@@ -8,15 +8,14 @@ import de.prob.statespace.AnimationSelector;
 import de.prob.statespace.Trace;
 import de.prob.statespace.Transition;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 
-import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
 import io.github.spencerpark.jupyter.kernel.display.DisplayData;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public final class TraceCommand implements Command {
 	private final @NotNull AnimationSelector animationSelector;
@@ -95,7 +94,7 @@ public final class TraceCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/TypeCommand.java b/src/main/java/de/prob2/jupyter/commands/TypeCommand.java
index 61c1843..09e466f 100644
--- a/src/main/java/de/prob2/jupyter/commands/TypeCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/TypeCommand.java
@@ -14,12 +14,12 @@ import de.prob.statespace.Trace;
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.CommandUtils;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -84,7 +84,9 @@ public final class TypeCommand implements Command {
 	}
 	
 	@Override
-	public @NotNull ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return CommandUtils.completeInBExpression(this.animationSelector.getCurrentTrace(), argString, at);
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return new ParameterCompleters(Collections.singletonMap(
+			FORMULA_PARAM, CommandUtils.bExpressionCompleter(this.animationSelector.getCurrentTrace())
+		));
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/UnletCommand.java b/src/main/java/de/prob2/jupyter/commands/UnletCommand.java
index 2d6960d..67f6644 100644
--- a/src/main/java/de/prob2/jupyter/commands/UnletCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/UnletCommand.java
@@ -8,13 +8,13 @@ import com.google.inject.Injector;
 
 import de.prob2.jupyter.Command;
 import de.prob2.jupyter.Parameter;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
@@ -75,8 +75,8 @@ public final class UnletCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
+	public @NotNull ParameterCompleters getParameterCompleters() {
 		// TODO
-		return null;
+		return ParameterCompleters.NONE;
 	}
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/VersionCommand.java b/src/main/java/de/prob2/jupyter/commands/VersionCommand.java
index 147296d..48bf67a 100644
--- a/src/main/java/de/prob2/jupyter/commands/VersionCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/VersionCommand.java
@@ -7,16 +7,15 @@ import de.prob.Main;
 import de.prob.animator.command.GetVersionCommand;
 import de.prob.statespace.AnimationSelector;
 import de.prob2.jupyter.Command;
+import de.prob2.jupyter.ParameterCompleters;
 import de.prob2.jupyter.ParameterInspectors;
 import de.prob2.jupyter.Parameters;
 import de.prob2.jupyter.ParsedArguments;
 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;
-import org.jetbrains.annotations.Nullable;
 
 public final class VersionCommand implements Command {
 	private final @NotNull AnimationSelector animationSelector;
@@ -83,7 +82,7 @@ public final class VersionCommand implements Command {
 	}
 	
 	@Override
-	public @Nullable ReplacementOptions complete(final @NotNull String argString, final int at) {
-		return null;
+	public @NotNull ParameterCompleters getParameterCompleters() {
+		return ParameterCompleters.NONE;
 	}
 }
-- 
GitLab