SHL-155: Propagate nb of TAB invocations to converter
- rename NB_INVOCATIONS_OPTION_CONTEXT_PREFIX to TAB_COMPLETION_COUNT_PREFIX along with value change
This commit is contained in:
@@ -20,38 +20,39 @@ import java.util.List;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell.core.annotation.CliOption;
|
||||
|
||||
|
||||
/**
|
||||
* Converts between Strings (as displayed by and entered via the shell) and Java objects
|
||||
*
|
||||
*
|
||||
* @author Ben Alex
|
||||
* @param <T> the type being converted to/from
|
||||
*/
|
||||
public interface Converter<T> {
|
||||
|
||||
/**
|
||||
* The prefix for the option context property that indicates how many successive tab completion requests have
|
||||
* occurred.
|
||||
*/
|
||||
public static final String TAB_COMPLETION_COUNT_PREFIX = "tab-completion-count-";
|
||||
|
||||
/**
|
||||
* Indicates whether this converter supports the given type in the given option context
|
||||
*
|
||||
*
|
||||
* @param type the type being checked
|
||||
* @param optionContext a non-<code>null</code> string that customises the
|
||||
* behaviour of this converter for a given {@link CliOption} of a given
|
||||
* {@link CliCommand}; the contents will have special meaning to this
|
||||
* converter (e.g. be a comma-separated list of keywords known to this
|
||||
* converter)
|
||||
* @param optionContext a non-<code>null</code> string that customises the behaviour of this converter for a given
|
||||
* {@link CliOption} of a given {@link CliCommand}; the contents will have special meaning to this converter (e.g.
|
||||
* be a comma-separated list of keywords known to this converter)
|
||||
* @return see above
|
||||
*/
|
||||
boolean supports(Class<?> type, String optionContext);
|
||||
|
||||
/**
|
||||
* Converts from the given String value to type T
|
||||
*
|
||||
*
|
||||
* @param value the value to convert
|
||||
* @param targetType the type being converted to; can't be <code>null</code>
|
||||
* @param optionContext a non-<code>null</code> string that customises the
|
||||
* behaviour of this converter for a given {@link CliOption} of a given
|
||||
* {@link CliCommand}; the contents will have special meaning to this
|
||||
* converter (e.g. be a comma-separated list of keywords known to this
|
||||
* converter)
|
||||
* @param optionContext a non-<code>null</code> string that customises the behaviour of this converter for a given
|
||||
* {@link CliOption} of a given {@link CliCommand}; the contents will have special meaning to this converter (e.g.
|
||||
* be a comma-separated list of keywords known to this converter)
|
||||
* @return see above
|
||||
* @throws RuntimeException if the given value could not be converted
|
||||
*/
|
||||
@@ -59,19 +60,17 @@ public interface Converter<T> {
|
||||
|
||||
/**
|
||||
* Populates the given list with the possible completions
|
||||
*
|
||||
*
|
||||
* @param completions the list to populate; can't be <code>null</code>
|
||||
* @param targetType the type of parameter for which a string is being entered
|
||||
* @param existingData what the user has typed so far
|
||||
* @param optionContext a non-<code>null</code> string that customises the
|
||||
* behaviour of this converter for a given {@link CliOption} of a given
|
||||
* {@link CliCommand}; the contents will have special meaning to this
|
||||
* converter (e.g. be a comma-separated list of keywords known to this
|
||||
* converter)
|
||||
* @param optionContext a non-<code>null</code> string that customises the behaviour of this converter for a given
|
||||
* {@link CliOption} of a given {@link CliCommand}; the contents will have special meaning to this converter (e.g.
|
||||
* be a comma-separated list of keywords known to this converter)
|
||||
* @param target
|
||||
* @return <code>true</code> if all the added completions are complete
|
||||
* values, or <code>false</code> if the user can press TAB to add further
|
||||
* information to some or all of them
|
||||
* @return <code>true</code> if all the added completions are complete values, or <code>false</code> if the user can
|
||||
* press TAB to add further information to some or all of them
|
||||
*/
|
||||
boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData, String optionContext, MethodTarget target);
|
||||
boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData,
|
||||
String optionContext, MethodTarget target);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,16 @@ public class SimpleParser implements Parser {
|
||||
|
||||
private final Map<String, MethodTarget> availabilityIndicators = new HashMap<String, MethodTarget>();
|
||||
|
||||
/**
|
||||
* The last buffer when completion was requested.
|
||||
*/
|
||||
private String previousCompletionBuffer;
|
||||
|
||||
/**
|
||||
* The number of times completion has been requested with the same buffer.
|
||||
*/
|
||||
private int successiveCompletionRequests = 1;
|
||||
|
||||
private MethodTarget getAvailabilityIndicator(final String command) {
|
||||
return availabilityIndicators.get(command);
|
||||
}
|
||||
@@ -109,6 +119,8 @@ public class SimpleParser implements Parser {
|
||||
Assert.notNull(rawInput, "Raw input required");
|
||||
final String input = normalise(rawInput);
|
||||
|
||||
resetCompletionInvocations();
|
||||
|
||||
// Locate the applicable targets which match this buffer
|
||||
final Collection<MethodTarget> matchingTargets = locateTargets(input, true, true);
|
||||
if (matchingTargets.isEmpty()) {
|
||||
@@ -315,6 +327,15 @@ public class SimpleParser implements Parser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We're being asked to execute a command, so the next completion invocation will definitely refer to a different
|
||||
* buffer.
|
||||
*/
|
||||
private void resetCompletionInvocations() {
|
||||
previousCompletionBuffer = null;
|
||||
successiveCompletionRequests = 1;
|
||||
}
|
||||
|
||||
private void reportTokenizingException(String commandKey, TokenizingException te) {
|
||||
StringBuilder caret = new StringBuilder();
|
||||
for (int i = 0; i < te.getOffendingOffset() + commandKey.length() + 1; i++) {
|
||||
@@ -565,15 +586,11 @@ public class SimpleParser implements Parser {
|
||||
cursor--;
|
||||
}
|
||||
|
||||
// Replace all multiple spaces with a single space
|
||||
while (buffer.contains(" ")) {
|
||||
buffer = buffer.replaceFirst(" ", " ");
|
||||
cursor--;
|
||||
}
|
||||
|
||||
// Begin by only including the portion of the buffer represented to the present cursor position
|
||||
String translated = buffer.substring(0, cursor);
|
||||
|
||||
String successiveInvocationContext = trackSuccessiveCompletionRequests(translated);
|
||||
|
||||
// Start by locating a method that matches
|
||||
final Collection<MethodTarget> targets = locateTargets(translated, false, true);
|
||||
SortedSet<Completion> results = new TreeSet<Completion>(COMPARATOR);
|
||||
@@ -834,10 +851,11 @@ public class SimpleParser implements Parser {
|
||||
}
|
||||
// Let's use a Converter if one is available
|
||||
for (Converter<?> candidate : converters) {
|
||||
if (candidate.supports(parameterType, option.optionContext())) {
|
||||
String optionContext = successiveInvocationContext + " " + option.optionContext();
|
||||
if (candidate.supports(parameterType, optionContext)) {
|
||||
// Found a usable converter
|
||||
boolean allComplete = candidate.getAllPossibleValues(allValues, parameterType,
|
||||
lastOptionValue, option.optionContext(), methodTarget);
|
||||
lastOptionValue, optionContext, methodTarget);
|
||||
if (!allComplete) {
|
||||
suffix = "";
|
||||
}
|
||||
@@ -892,6 +910,22 @@ public class SimpleParser implements Parser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the number of times completion has been requested for the same buffer, resetting everytime the buffer
|
||||
* changes.
|
||||
* @return the portion of "option context" that indicates the number of invocation
|
||||
*/
|
||||
private String trackSuccessiveCompletionRequests(String translated) {
|
||||
if (translated.equals(previousCompletionBuffer)) {
|
||||
successiveCompletionRequests++;
|
||||
}
|
||||
else {
|
||||
previousCompletionBuffer = translated;
|
||||
successiveCompletionRequests = 1;
|
||||
}
|
||||
return Converter.TAB_COMPLETION_COUNT_PREFIX + successiveCompletionRequests;
|
||||
}
|
||||
|
||||
private void displayHelp(String lastOptionKey, CliOption option) {
|
||||
StringBuilder help = new StringBuilder();
|
||||
help.append(OsUtils.LINE_SEPARATOR);
|
||||
|
||||
@@ -30,10 +30,12 @@ import static org.junit.Assert.assertThat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.DiagnosingMatcher;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell.core.annotation.CliOption;
|
||||
@@ -369,6 +371,46 @@ public class SimpleParserTests {
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 \"def\" ")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessiveInvocationsOfCompletion() {
|
||||
|
||||
parser.add(new MyCommands());
|
||||
parser.add(new ExpertCompletions());
|
||||
|
||||
buffer = "bar --option1 ";
|
||||
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
|
||||
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 one ")))));
|
||||
assertThat(candidates, not(hasItem(completionThat(is(equalTo("bar --option1 two "))))));
|
||||
assertThat(candidates, not(hasItem(completionThat(is(equalTo("bar --option1 three "))))));
|
||||
|
||||
candidates.clear();
|
||||
// Simulate <TAB> twice
|
||||
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
|
||||
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 one ")))));
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 two ")))));
|
||||
assertThat(candidates, not(hasItem(completionThat(is(equalTo("bar --option1 three "))))));
|
||||
|
||||
candidates.clear();
|
||||
// Simulate <TAB> three times
|
||||
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
|
||||
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 one ")))));
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 two ")))));
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("bar --option1 three ")))));
|
||||
|
||||
// And now for something completely different
|
||||
buffer = "testMandatory --option2 ";
|
||||
candidates.clear();
|
||||
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
|
||||
|
||||
assertThat(candidates, hasItem(completionThat(is(equalTo("testMandatory --option2 one ")))));
|
||||
assertThat(candidates, not(hasItem(completionThat(is(equalTo("testMandatory --option2 two "))))));
|
||||
assertThat(candidates, not(hasItem(completionThat(is(equalTo("testMandatory --option2 three "))))));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://jira.spring.io/browse/SHL-113
|
||||
*/
|
||||
@@ -517,4 +559,34 @@ public class SimpleParserTests {
|
||||
|
||||
}
|
||||
|
||||
public static class ExpertCompletions implements Converter<String> {
|
||||
|
||||
private static final Pattern NUMBER_OF_COMPLETIONS_CAPTURE = Pattern.compile(".*completion-count-(\\d+).*");
|
||||
|
||||
private static final String[] results = new String[] { "one", "two", "three" };
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type, String optionContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertFromText(String value, Class<?> targetType, String optionContext) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData,
|
||||
String optionContext, MethodTarget target) {
|
||||
java.util.regex.Matcher m = NUMBER_OF_COMPLETIONS_CAPTURE.matcher(optionContext);
|
||||
Assert.assertTrue(m.matches());
|
||||
int invocations = Integer.parseInt(m.group(1));
|
||||
for (int i = 0; i < invocations; i++) {
|
||||
completions.add(new Completion(results[i]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user