From 137889ebb1c9384c99534cf98fc6a057acc95099 Mon Sep 17 00:00:00 2001
From: dgelessus <dgelessus@users.noreply.github.com>
Date: Thu, 19 Jul 2018 16:55:54 +0200
Subject: [PATCH] Add initial support for inspection (Shift-Tab in Jupyter
 Notebook)

---
 notebooks/tests/help.ipynb                    | 14 ++++++--
 .../java/de/prob2/jupyter/ProBKernel.java     | 32 +++++++++++++++++++
 .../de/prob2/jupyter/commands/Command.java    | 30 ++++++++++++++++-
 .../prob2/jupyter/commands/HelpCommand.java   | 14 +-------
 4 files changed, 74 insertions(+), 16 deletions(-)

diff --git a/notebooks/tests/help.ipynb b/notebooks/tests/help.ipynb
index 8489fd0..c37e3a7 100644
--- a/notebooks/tests/help.ipynb
+++ b/notebooks/tests/help.ipynb
@@ -21,10 +21,13 @@
        "* `::render` Render some content with the specified MIME type.\n",
        "* `:assert` Ensure that the predicate is true, and show an error otherwise.\n",
        "* `:browse` Show information about the current state.\n",
+       "* `:check` Check the machine's properties, invariant, or assertions in the current state.\n",
        "* `:constants` Set up the current machine's constants.\n",
        "* `:dot` Execute and show a dot visualisation.\n",
        "* `:eval` Evaluate a formula and display the result.\n",
-       "* `:exec` Execute an operation with the specified predicate, or by its ID.\n",
+       "* `:exec` Execute an operation.\n",
+       "* `:find` Try to find a state for which the given predicate is true (in addition to the machine's invariant).\n",
+       "* `:goto` Go to the state with the specified index in the current trace.\n",
        "* `:groovy` Evaluate the given Groovy expression.\n",
        "* `:help` Display help for a specific command, or general help about the REPL.\n",
        "* `:init` Initialise the current machine with the specified predicate\n",
@@ -33,8 +36,10 @@
        "* `:prettyprint` Pretty-print a predicate.\n",
        "* `:show` Show the machine's animation function visualisation for the current state.\n",
        "* `:solve` Solve a predicate with the specified solver.\n",
+       "* `:stats` Show statistics about the state space.\n",
        "* `:table` Display an expression as a table.\n",
        "* `:time` Execute the given command and measure how long it takes to execute.\n",
+       "* `:trace` Display all states and transitions in the current trace.\n",
        "* `:type` Display the type of a formula.\n",
        "* `:version` Display version info about the ProB CLI and ProB 2.\n"
       ],
@@ -44,10 +49,13 @@
        "::render Render some content with the specified MIME type.\n",
        ":assert Ensure that the predicate is true, and show an error otherwise.\n",
        ":browse Show information about the current state.\n",
+       ":check Check the machine's properties, invariant, or assertions in the current state.\n",
        ":constants Set up the current machine's constants.\n",
        ":dot Execute and show a dot visualisation.\n",
        ":eval Evaluate a formula and display the result.\n",
-       ":exec Execute an operation with the specified predicate, or by its ID.\n",
+       ":exec Execute an operation.\n",
+       ":find Try to find a state for which the given predicate is true (in addition to the machine's invariant).\n",
+       ":goto Go to the state with the specified index in the current trace.\n",
        ":groovy Evaluate the given Groovy expression.\n",
        ":help Display help for a specific command, or general help about the REPL.\n",
        ":init Initialise the current machine with the specified predicate\n",
@@ -56,8 +64,10 @@
        ":prettyprint Pretty-print a predicate.\n",
        ":show Show the machine's animation function visualisation for the current state.\n",
        ":solve Solve a predicate with the specified solver.\n",
+       ":stats Show statistics about the state space.\n",
        ":table Display an expression as a table.\n",
        ":time Execute the given command and measure how long it takes to execute.\n",
+       ":trace Display all states and transitions in the current trace.\n",
        ":type Display the type of a formula.\n",
        ":version Display version info about the ProB CLI and ProB 2.\n"
       ]
diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index 46846d6..df2b0cf 100644
--- a/src/main/java/de/prob2/jupyter/ProBKernel.java
+++ b/src/main/java/de/prob2/jupyter/ProBKernel.java
@@ -254,6 +254,38 @@ public final class ProBKernel extends BaseKernel {
 		return this.executeCommand(":eval", expr);
 	}
 	
+	@Override
+	public @Nullable DisplayData inspect(final @NotNull String code, final int at, final boolean extraDetail) {
+		// Note: We ignore the extraDetail parameter, because in practice it is always false. This is because the inspect_request messages sent by Jupyter Notebook always have their detail_level set to 0.
+		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code);
+		if (commandMatcher.matches()) {
+			// The code is a valid command.
+			final int argOffset = commandMatcher.start(2);
+			final String name = commandMatcher.group(1);
+			if (this.getCommands().containsKey(name)) {
+				final Command command = this.getCommands().get(name);
+				if (at <= commandMatcher.end(1)) {
+					// The cursor is somewhere in the command name, show help text for the command.
+					return command.renderHelp();
+				} else if (at < commandMatcher.start(2)) {
+					// The cursor is in the whitespace between the command name and arguments, don't show anything.
+					return null;
+				} else {
+					// The cursor is somewhere in the command arguments, ask the command to inspect.
+					assert name != null;
+					final String argString = commandMatcher.group(2) == null ? "" : commandMatcher.group(2);
+					return command.inspect(argString, at - argOffset);
+				}
+			} else {
+				// Invalid command, can't inspect.
+				return null;
+			}
+		} else {
+			// The code is not a valid command, ask :eval to inspect.
+			return this.getCommands().get(":eval").inspect(code, at);
+		}
+	}
+	
 	@Override
 	public @Nullable ReplacementOptions complete(final @NotNull String code, final int at) {
 		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code);
diff --git a/src/main/java/de/prob2/jupyter/commands/Command.java b/src/main/java/de/prob2/jupyter/commands/Command.java
index b9be2eb..fb906dc 100644
--- a/src/main/java/de/prob2/jupyter/commands/Command.java
+++ b/src/main/java/de/prob2/jupyter/commands/Command.java
@@ -11,9 +11,37 @@ public interface Command {
 	
 	public abstract @NotNull String getShortHelp();
 	
-	public abstract  @NotNull String getHelpBody();
+	public abstract @NotNull String getHelpBody();
+	
+	public default @NotNull DisplayData renderHelp() {
+		final StringBuilder sbPlain = new StringBuilder();
+		final StringBuilder sbMarkdown = new StringBuilder();
+		sbPlain.append(this.getSyntax());
+		sbPlain.append('\n');
+		sbMarkdown.append("```\n");
+		sbMarkdown.append(this.getSyntax());
+		sbMarkdown.append("\n```\n\n");
+		
+		final String shortHelp = this.getShortHelp();
+		sbPlain.append(shortHelp);
+		sbMarkdown.append(shortHelp);
+		final String helpBody = this.getHelpBody();
+		if (!helpBody.isEmpty()) {
+			sbPlain.append("\n\n");
+			sbPlain.append(helpBody);
+			sbMarkdown.append("\n\n");
+			sbMarkdown.append(helpBody);
+		}
+		final DisplayData result = new DisplayData(sbPlain.toString());
+		result.putMarkdown(sbMarkdown.toString());
+		return result;
+	}
 	
 	public abstract @Nullable DisplayData run(final @NotNull String argString);
 	
+	public default @Nullable DisplayData inspect(final @NotNull String argString, final int at) {
+		return null;
+	}
+	
 	public abstract @Nullable ReplacementOptions complete(final @NotNull String argString, final int at);
 }
diff --git a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
index 42fb943..bfee912 100644
--- a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java
@@ -77,19 +77,7 @@ public final class HelpCommand implements Command {
 			if (command == null) {
 				throw new UserErrorException(String.format("Cannot display help for unknown command \"%s\"", commandName));
 			}
-			final StringBuilder sb = new StringBuilder();
-			sb.append("```\n");
-			sb.append(command.getSyntax());
-			sb.append("\n```\n\n");
-			sb.append(command.getShortHelp());
-			final String helpBody = command.getHelpBody();
-			if (!helpBody.isEmpty()) {
-				sb.append("\n\n");
-				sb.append(helpBody);
-			}
-			final DisplayData result = new DisplayData(sb.toString());
-			result.putMarkdown(sb.toString());
-			return result;
+			return command.renderHelp();
 		} else {
 			throw new UserErrorException("Expected at most 1 argument, got " + args.size());
 		}
-- 
GitLab