diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java
index 675f7439965..eaf78a2c5a5 100644
--- a/src/main/java/seedu/address/commons/util/StringUtil.java
+++ b/src/main/java/seedu/address/commons/util/StringUtil.java
@@ -86,4 +86,78 @@ public static String formatWithNullFallback(String format, Object... values) {
return String.format(format, values);
}
+
+ /**
+ * Returns true if the inputString
is a fuzzy match of the targetString
,
+ * false otherwise.
+ *
+ *
+ * A fuzzy search is an approximate search algorithm. This implementation computes a fuzzy match by determining
+ * if there exists a subsequence match in linear time.
+ *
+ *
+ *
+ * As an example, "abc"
is considered to be a fuzzy match of "aa1b2ccc"
, since one may
+ * construct the subsequence "abc"
by removing extra characters "a1"
, "2cc"
+ * from aa1b2ccc
.
+ *
+ *
+ * @param inputString The partial fuzzy input string.
+ * @param targetString The target string to check if the input fuzzily matches with.
+ * @return true if the input fuzzy-matches (is fuzzily contained in) the target, false otherwise.
+ */
+ public static boolean isFuzzyMatch(String inputString, String targetString) {
+ if (inputString == null || targetString == null) {
+ return inputString == null && targetString == null; // both null => true, otherwise false
+ }
+
+ int inputI = 0;
+ int targetI = 0;
+
+ while (inputI < inputString.length() && targetI < targetString.length()) {
+ char c = inputString.charAt(inputI);
+ char t = targetString.charAt(targetI);
+
+ if (c == t) {
+ inputI++;
+ }
+ targetI++;
+ }
+
+ return inputI >= inputString.length();
+ }
+
+ /**
+ * Returns a score representing how close it is to matching characters at the beginning of the target.
+ * The higher the value, the better it is. A failed match will have {@code -targetString.length()}, while
+ * a complete match will have {@code inputString.length()}.
+ *
+ */
+ public static int getFuzzyMatchScore(String inputString, String targetString) {
+ if (inputString == null || targetString == null) {
+ return 0;
+ }
+
+ int inputI = 0;
+ int targetI = 0;
+
+ int errorCount = 0;
+
+ while (inputI < inputString.length() && targetI < targetString.length()) {
+ char c = inputString.charAt(inputI);
+ char t = targetString.charAt(targetI);
+
+ if (c == t) {
+ errorCount += targetI - inputI - errorCount;
+ inputI++;
+ }
+ targetI++;
+ }
+
+ if (inputI < inputString.length()) {
+ return -targetString.length();
+ }
+
+ return inputI - errorCount;
+ }
}
diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java
index d7262dbdd56..430c98f41b2 100644
--- a/src/main/java/seedu/address/logic/Logic.java
+++ b/src/main/java/seedu/address/logic/Logic.java
@@ -1,6 +1,7 @@
package seedu.address.logic;
import java.nio.file.Path;
+import java.util.stream.Stream;
import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
@@ -23,6 +24,13 @@ public interface Logic {
*/
CommandResult execute(String commandText) throws CommandException, ParseException;
+ /**
+ * Parses the command and returns any autocompletion results.
+ * @param commandText The command as entered by the user.
+ * @return the result of the command execution.
+ */
+ Stream generateCompletions(String commandText);
+
/**
* Returns the AddressBook.
*
diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java
index de120e152b9..7cbd4b7a3c1 100644
--- a/src/main/java/seedu/address/logic/LogicManager.java
+++ b/src/main/java/seedu/address/logic/LogicManager.java
@@ -4,6 +4,7 @@
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.util.logging.Logger;
+import java.util.stream.Stream;
import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
@@ -11,7 +12,7 @@
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.exceptions.CommandException;
-import seedu.address.logic.parser.AddressBookParser;
+import seedu.address.logic.parser.AppParser;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
@@ -31,7 +32,7 @@ public class LogicManager implements Logic {
private final Model model;
private final Storage storage;
- private final AddressBookParser addressBookParser;
+ private final AppParser appParser;
/**
* Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}.
@@ -39,7 +40,7 @@ public class LogicManager implements Logic {
public LogicManager(Model model, Storage storage) {
this.model = model;
this.storage = storage;
- addressBookParser = new AddressBookParser();
+ appParser = new AppParser();
}
@Override
@@ -47,7 +48,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
logger.info("----------------[USER COMMAND][" + commandText + "]");
CommandResult commandResult;
- Command command = addressBookParser.parseCommand(commandText);
+ Command command = appParser.parseCommand(commandText);
commandResult = command.execute(model);
try {
@@ -61,6 +62,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
return commandResult;
}
+ @Override
+ public Stream generateCompletions(String commandText) {
+ return appParser
+ .parseCompletionGenerator(commandText)
+ .generateCompletions(commandText, model);
+ }
+
@Override
public ReadOnlyAddressBook getAddressBook() {
return model.getAddressBook();
diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java
new file mode 100644
index 00000000000..5ac24d9d05b
--- /dev/null
+++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java
@@ -0,0 +1,167 @@
+package seedu.address.logic.autocomplete;
+
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import seedu.address.commons.util.StringUtil;
+import seedu.address.logic.parser.Flag;
+import seedu.address.model.Model;
+
+/**
+ * Creates a generator based on the given supplier or reference commands, so as it can generate
+ * auto-completions when requested.
+ */
+public class AutocompleteGenerator {
+
+ /** An autocompletion generator that generates no results. */
+ public static final AutocompleteGenerator NO_RESULTS = new AutocompleteGenerator(Stream::empty);
+
+
+ /** A comparator used to order fuzzily matched strings where better matches against the input go first. */
+ private static final Function> TEXT_FUZZY_MATCH_COMPARATOR = (input) -> (s1, s2) -> {
+ // Get how well s1 is ahead of s2 (note: higher is better).
+ int score = StringUtil.getFuzzyMatchScore(input, s1) - StringUtil.getFuzzyMatchScore(input, s2);
+
+ return -score; // -ve implies s1 < s2
+ };
+
+ /** A comparator used to order fuzzily matched flags where better matches against the input go first. */
+ private static final Function> FLAG_FUZZY_MATCH_COMPARATOR = (input) -> (f1, f2) -> {
+ // Get how well f1 is ahead of f2 in both metrics (note: higher is better).
+ int score = Math.max(
+ StringUtil.getFuzzyMatchScore(input, f1.getFlagString()),
+ StringUtil.getFuzzyMatchScore(input, f1.getFlagAliasString())
+ ) - Math.max(
+ StringUtil.getFuzzyMatchScore(input, f2.getFlagString()),
+ StringUtil.getFuzzyMatchScore(input, f2.getFlagAliasString())
+ );
+
+ return -score; // -ve implies f1 < f2
+ };
+
+
+
+ /** The cached instance of the result evaluation function. */
+ private final BiFunction> resultEvaluationFunction;
+
+ /**
+ * Constructs an autocomplete generator based on the given set of reference full command strings.
+ */
+ public AutocompleteGenerator(Stream referenceCommands) {
+ this(() -> referenceCommands);
+ }
+
+ /**
+ * Constructs an autocomplete generator based on the given supplier of full command strings.
+ */
+ public AutocompleteGenerator(Supplier> referenceCommandsSupplier) {
+ resultEvaluationFunction = (partialCommand, model) -> {
+ if (partialCommand == null) {
+ return Stream.empty();
+ }
+
+ return referenceCommandsSupplier.get()
+ .filter(term -> StringUtil.isFuzzyMatch(partialCommand, term))
+ .sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(partialCommand))
+ .distinct();
+ };
+ }
+
+ /**
+ * Constructs an autocomplete generator based on the given {@link AutocompleteSupplier}.
+ */
+ public AutocompleteGenerator(AutocompleteSupplier supplier) {
+ resultEvaluationFunction = (partialCommand, model) -> {
+
+ PartitionedCommand command = new PartitionedCommand(partialCommand == null ? "" : partialCommand);
+ String trailingText = command.getTrailingText();
+
+ // Compute the possible flags and flag-values.
+ Stream possibleFlags = getPossibleFlags(command, supplier)
+ .filter(flag -> StringUtil.isFuzzyMatch(trailingText, flag.getFlagString())
+ || StringUtil.isFuzzyMatch(trailingText, flag.getFlagAliasString()))
+ .sorted(FLAG_FUZZY_MATCH_COMPARATOR.apply(trailingText))
+ .map(Flag::getFlagString);
+
+ Stream possibleValues = getPossibleValues(command, supplier, model)
+ .map(s -> s.filter(term -> StringUtil.isFuzzyMatch(trailingText, term))
+ .sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(trailingText)))
+ .orElse(possibleFlags);
+
+ // Decide which stream to use based on whether it's of a flag syntax or not.
+ Stream possibleTerminalValues;
+ if (command.hasFlagSyntaxPrefixInTrailingText()) {
+ possibleTerminalValues = possibleFlags;
+ } else {
+ possibleTerminalValues = possibleValues;
+ }
+
+ // Return the results as a full completion string, distinct.
+ return possibleTerminalValues
+ .map(command::toStringWithNewTrailingTerm)
+ .distinct();
+ };
+ }
+
+ /**
+ * Generates a stream of completions based on the partial command given and no model.
+ * Note that omitting the model may limit the ability to return useful results.
+ */
+ public Stream generateCompletions(String command) {
+ return resultEvaluationFunction.apply(command, null);
+ }
+
+ /**
+ * Generates a stream of completions based on the partial command given and the model.
+ */
+ public Stream generateCompletions(String command, Model model) {
+ return resultEvaluationFunction.apply(command, model);
+ }
+
+
+
+
+ /**
+ * Obtains the set of possible flags based on the partitioned command and supplier.
+ */
+ private static Stream getPossibleFlags(
+ PartitionedCommand command,
+ AutocompleteSupplier supplier
+ ) {
+ Flag[] allPossibleFlags = supplier.getAllPossibleFlags().toArray(Flag[]::new);
+
+ Set existingCommandFlags = command.getConfirmedFlagStrings()
+ .stream()
+ .map(flagStr -> Flag.findMatch(flagStr, allPossibleFlags))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+
+
+ return supplier.getOtherPossibleFlagsAsideFromFlagsPresent(existingCommandFlags).stream();
+ }
+
+ /**
+ * Obtains the optional set of possible values based on the partitioned command, supplier, and model.
+ * If this optional is empty, that means it is explicitly specified that the flag cannot accept values.
+ */
+ private static Optional> getPossibleValues(
+ PartitionedCommand command,
+ AutocompleteSupplier supplier,
+ Model model
+ ) {
+ return supplier.getValidValues(
+ Flag.parseOptional(
+ command.getLastConfirmedFlagString().orElse(null)
+ ).orElse(null),
+ model
+ );
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java
new file mode 100644
index 00000000000..820109a3441
--- /dev/null
+++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java
@@ -0,0 +1,145 @@
+package seedu.address.logic.autocomplete;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import seedu.address.logic.autocomplete.data.AutocompleteDataSet;
+import seedu.address.logic.parser.Flag;
+import seedu.address.model.Model;
+
+/**
+ * Supplies autocompletion details for arbitrary commands.
+ */
+public class AutocompleteSupplier {
+
+ private final AutocompleteDataSet flags;
+ private final Map values;
+
+ /**
+ * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command.
+ * The ordering supplied via the parameters are followed when generating results.
+ *
+ * @param flags The set of flags that should be used as part of the autocomplete results.
+ * @param values A map of auto-completable values for each flag that may be obtained via a model.
+ */
+ public AutocompleteSupplier(
+ AutocompleteDataSet flags,
+ Map values
+ ) {
+ // Create new copies to prevent external modification.
+ this.flags = flags.copy();
+ this.values = new LinkedHashMap<>(values);
+ }
+
+ /**
+ * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command.
+ * The ordering supplied via the parameters are followed when generating results.
+ *
+ * @param flags The set of flags that should be used as part of the autocomplete results.
+ *
+ * @see #AutocompleteSupplier(AutocompleteDataSet, Map)
+ */
+ public static AutocompleteSupplier from(AutocompleteDataSet flags) {
+ return new AutocompleteSupplier(flags, Map.of());
+ }
+
+ /**
+ * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command.
+ * The ordering supplied via the parameters are followed when generating results.
+ *
+ * @param flagSets The sets of flags that should be used together as part of the autocomplete results.
+ */
+ @SafeVarargs
+ public static AutocompleteSupplier from(AutocompleteDataSet... flagSets) {
+ return from(AutocompleteDataSet.concat(flagSets));
+ }
+
+ /**
+ * Constructs an autocomplete supplier that is capable of supplying all the unique flags (flags which may appear
+ * at most once) for autocompleting some command.
+ * The ordering supplied via the parameters are followed when generating results.
+ *
+ * @param uniqueFlags A set of flags that each may appear at most once in the command.
+ */
+ public static AutocompleteSupplier fromUniqueFlags(Flag... uniqueFlags) {
+ return AutocompleteSupplier.from(
+ AutocompleteDataSet.onceForEachOf(uniqueFlags)
+ );
+ }
+
+ /**
+ * Constructs an autocomplete supplier that is capable of supplying all the repeatable flags (flags which may
+ * appear any number of times in the command) for autocompleting some command.
+ * The ordering supplied via the parameters are followed when generating results.
+ *
+ * @param repeatableFlags A set of flags that may appear at any point in the command any number of times.
+ */
+ public static AutocompleteSupplier fromRepeatableFlags(Flag... repeatableFlags) {
+ return AutocompleteSupplier.from(
+ AutocompleteDataSet.anyNumberOf(repeatableFlags)
+ );
+ }
+
+ /**
+ * Returns a set of all possible flags that can be used in the command.
+ * The set has predictable iteration order: it follows the ordering supplied via the original inputs.
+ */
+ public Set getAllPossibleFlags() {
+ return flags.copy();
+ }
+
+ /**
+ * Returns a set of other possible flags given the list of flags that are already present in the command.
+ * If there are conflicting constraints specified, this will use the tightest possible constraint.
+ * The set has predictable iteration order: it follows the ordering supplied via the original inputs.
+ */
+ public Set getOtherPossibleFlagsAsideFromFlagsPresent(Set flagsPresent) {
+ return flags.getElementsAfterConsuming(flagsPresent);
+ }
+
+ /**
+ * Returns an optional stream of possible values for a flag when computed against a given model.
+ * If this optional is empty, then this flag is explicitly specified to not have any values,
+ * and not just the lack of completion suggestions.
+ *
+ * @param flag The flag to check against. This may be null to represent the preamble.
+ * @param model The model to be supplied for generation. This may be null if model-data is not essential
+ * for any purpose.
+ */
+ public Optional> getValidValues(Flag flag, Model model) {
+ try {
+ return Optional.ofNullable(
+ this.values.getOrDefault(flag, m -> Stream.of())
+ ).map(x -> x.apply(model));
+
+ } catch (RuntimeException e) {
+ // Guard against errors like NPEs due to supplied lambdas not handling them.
+ e.printStackTrace();
+ // We simply return that we don't know what to auto-complete by.
+ return Optional.of(Stream.of());
+ }
+ }
+
+ /**
+ * Configures the set of flags within this autocomplete supplier using the given {@code operator}.
+ * This also returns {@code this} instance, which is useful for chaining.
+ */
+ public AutocompleteSupplier configureFlagSet(Consumer> operator) {
+ operator.accept(this.flags);
+ return this;
+ }
+
+ /**
+ * Configures the map of values within this autocomplete supplier using the given {@code operator}.
+ * This also returns {@code this} instance, which is useful for chaining.
+ */
+ public AutocompleteSupplier configureValueMap(Consumer