diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java index 08dc822f2f9f9cab29bc71b648f7a566e144aaff..28d8ca71f4c003bd65e1fa5b3eadb2a81410231f 100644 --- a/src/main/java/de/prob2/jupyter/ProBKernel.java +++ b/src/main/java/de/prob2/jupyter/ProBKernel.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import com.google.inject.Inject; import com.google.inject.Injector; @@ -42,6 +43,7 @@ import de.prob2.jupyter.commands.VersionCommand; import io.github.spencerpark.jupyter.kernel.BaseKernel; import io.github.spencerpark.jupyter.kernel.LanguageInfo; +import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; import io.github.spencerpark.jupyter.kernel.display.mime.MIMEType; @@ -55,6 +57,7 @@ public final class ProBKernel extends BaseKernel { private static final @NotNull Logger LOGGER = LoggerFactory.getLogger(ProBKernel.class); private static final @NotNull Pattern COMMAND_PATTERN = Pattern.compile("\\s*(\\:[^\\s]*)(?:\\h*(.*))?", Pattern.DOTALL); + private static final @NotNull Pattern SPACE_PATTERN = Pattern.compile("\\s*"); private static final @NotNull Pattern BSYMB_COMMAND_PATTERN = Pattern.compile("\\\\([a-z]+)"); private static final @NotNull Map<@NotNull String, @NotNull String> BSYMB_COMMAND_DEFINITIONS; @@ -213,6 +216,55 @@ public final class ProBKernel extends BaseKernel { return this.executeCommand(":eval", expr); } + @Override + public @Nullable ReplacementOptions complete(final @NotNull String code, final int at) { + final Matcher commandMatcher = COMMAND_PATTERN.matcher(code); + if (commandMatcher.matches()) { + // The code is a valid command. + final int argOffset = commandMatcher.start(2); + if (at <= commandMatcher.end(1)) { + // The cursor is somewhere in the command name, provide command completions. + final String prefix = code.substring(commandMatcher.start(1), at); + return new ReplacementOptions( + this.getCommands().keySet().stream().filter(s -> s.startsWith(prefix)).sorted().collect(Collectors.toList()), + commandMatcher.start(1), + commandMatcher.end(1) + ); + } 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 provide completions. + final String name = commandMatcher.group(1); + 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(this, argString, at - argOffset); + return replacements == null ? null : new ReplacementOptions( + replacements.getReplacements(), + replacements.getSourceStart() + argOffset, + replacements.getSourceEnd() + argOffset + ); + } else { + // Invalid command, can't provide any completions. + return null; + } + } + } 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(this, code, at); + if (evalReplacements != null) { + replacementStrings.addAll(evalReplacements.getReplacements()); + } + replacementStrings.addAll(this.getCommands().keySet().stream().sorted().collect(Collectors.toList())); + return new ReplacementOptions(replacementStrings, at, at); + } else { + // The code is not a valid command, ask :eval for completions. + return this.getCommands().get(":eval").complete(this, code, at); + } + } + @Override public @NotNull LanguageInfo getLanguageInfo() { return new LanguageInfo.Builder("prob") diff --git a/src/main/java/de/prob2/jupyter/commands/Command.java b/src/main/java/de/prob2/jupyter/commands/Command.java index c55ed79bf7ff9168b067d70a2cdbf2bb57c2ce87..460e403afc4095847efc0f74e23c71eda7d56be0 100644 --- a/src/main/java/de/prob2/jupyter/commands/Command.java +++ b/src/main/java/de/prob2/jupyter/commands/Command.java @@ -2,6 +2,7 @@ package de.prob2.jupyter.commands; 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; @@ -13,4 +14,8 @@ public interface Command { public abstract @NotNull String getShortHelp(); public abstract @Nullable DisplayData run(final @NotNull ProBKernel kernel, final @NotNull String argString); + + public default @Nullable ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return null; + } } diff --git a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java index ae6cac670ba32c1c276bf0df478e190f769268bd..6b2fafaf18d98f015088ecb6adaade3eda55f004 100644 --- a/src/main/java/de/prob2/jupyter/commands/HelpCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/HelpCommand.java @@ -2,12 +2,14 @@ package de.prob2.jupyter.commands; import java.util.List; import java.util.TreeMap; +import java.util.stream.Collectors; import com.google.inject.Inject; 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; @@ -71,4 +73,14 @@ public final class HelpCommand implements Command { throw new UserErrorException("Expected at most 1 argument, got " + args.size()); } } + + @Override + public @NotNull ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + final String prefix = argString.substring(0, at); + return new ReplacementOptions( + kernel.getCommands().keySet().stream().filter(s -> s.startsWith(prefix)).sorted().collect(Collectors.toList()), + 0, + argString.length() + ); + } } diff --git a/src/main/java/de/prob2/jupyter/commands/TimeCommand.java b/src/main/java/de/prob2/jupyter/commands/TimeCommand.java index e9afce88c2450128c10a4ba629e7766f7fbddbc6..9148485ad81e64874adbd52ec594e46bb17f146b 100644 --- a/src/main/java/de/prob2/jupyter/commands/TimeCommand.java +++ b/src/main/java/de/prob2/jupyter/commands/TimeCommand.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; 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; @@ -39,4 +40,9 @@ public final class TimeCommand implements Command { kernel.display(timeDisplay); return result; } + + @Override + public @Nullable ReplacementOptions complete(final @NotNull ProBKernel kernel, final @NotNull String argString, final int at) { + return kernel.complete(argString, at); + } }