Refactor + only present not yet provided options

This commit is contained in:
Eric Bottard
2014-02-07 10:46:41 +01:00
parent e9bb3366a1
commit 7d8d9b2fde
4 changed files with 123 additions and 82 deletions

View File

@@ -658,7 +658,7 @@ public class SimpleParser implements Parser {
// Handle if they are trying to find out the available option keys; always present option keys in order
// of their declaration on the method signature, thus we can stop when mandatory options are filled in
if (methodTarget.getRemainingBuffer().endsWith("--") && !tokenizer.lastValueIsStillBeingTyped()) {
if (methodTarget.getRemainingBuffer().endsWith("--") && !tokenizer.openingQuotesHaveNotBeenClosed()) {
boolean showAllRemaining = true;
for (CliOption include : unspecified) {
if (include.mandatory()) {
@@ -685,7 +685,7 @@ public class SimpleParser implements Parser {
// option key/value pair)
if (lastOptionKey == null
|| (!"".equals(lastOptionKey) && !"".equals(lastOptionValue) && translated.endsWith(" ") && !tokenizer
.lastValueIsStillBeingTyped())) {
.openingQuotesHaveNotBeenClosed())) {
// We have either NEVER specified an option key/value pair
// OR we have specified a full option key/value pair
@@ -740,8 +740,8 @@ public class SimpleParser implements Parser {
}
}
// Only abort at this point if we have some suggestions; otherwise we might want to try to complete the
// "" option
// Only abort at this point if we have some suggestions;
// otherwise we might want to try to complete the "" option
if (results.size() > 0) {
candidates.addAll(results);
return 0;
@@ -749,13 +749,12 @@ public class SimpleParser implements Parser {
}
// Handle completing the option key they're presently typing
if ("".equals(lastOptionValue)) {
if (lastOptionKey != null && "".equals(lastOptionValue)) {
// Given we haven't got an option value of any form, we must
// still be typing an option key.
for (CliOption option : cliOptions) {
for (CliOption option : unspecified) {
for (String value : option.key()) {
if (value != null && lastOptionKey != null
&& value.regionMatches(true, 0, lastOptionKey, 0, lastOptionKey.length())) {
if (value != null && value.regionMatches(true, 0, lastOptionKey, 0, lastOptionKey.length())) {
String completionValue = translated.substring(0,
(translated.length() - lastOptionKey.length()))
+ value + " ";
@@ -795,85 +794,26 @@ public class SimpleParser implements Parser {
}
if (allValues.isEmpty()) {
// Doesn't appear to be a custom Converter, so let's go and provide defaults for simple
// types
// Provide some simple options for common types
if (Boolean.class.isAssignableFrom(parameterType)
|| Boolean.TYPE.isAssignableFrom(parameterType)) {
allValues.add(new Completion("true"));
allValues.add(new Completion("false"));
}
if (Number.class.isAssignableFrom(parameterType)) {
allValues.add(new Completion("0"));
allValues.add(new Completion("1"));
allValues.add(new Completion("2"));
allValues.add(new Completion("3"));
allValues.add(new Completion("4"));
allValues.add(new Completion("5"));
allValues.add(new Completion("6"));
allValues.add(new Completion("7"));
allValues.add(new Completion("8"));
allValues.add(new Completion("9"));
}
// Doesn't appear to be a custom Converter, so let's go and provide defaults
// for simple types
completeForSimpleTypes(parameterType, allValues);
}
// Only include in the candidates those results which are compatible with the present buffer
for (Completion currentValue : allValues) {
// We only provide a suggestion if the lastOptionValue == ""
if (!StringUtils.hasText(lastOptionValue)) {
// We should add the result, as they haven't typed anything yet
// Only add the result **if** what they've typed is compatible *AND* they haven't
// already typed it in full
if (currentValue.getValue().toLowerCase().startsWith(lastOptionValue.toLowerCase())
&& lastOptionValue.length() < currentValue.getValue().length()
&& (!tokenizer.lastValueIsComplete())) {
results.add(new Completion(currentValue.getValue() + suffix, currentValue
.getFormattedValue(), currentValue.getHeading(), currentValue.getOrder()));
}
else {
// Only add the result **if** what they've typed is compatible *AND* they haven't
// already typed it in full
if (currentValue.getValue().toLowerCase().startsWith(lastOptionValue.toLowerCase())
&& !lastOptionValue.equalsIgnoreCase(currentValue.getValue())
&& lastOptionValue.length() < currentValue.getValue().length()
&& (tokenizer.getLastValueDelimiter() == ' ' || tokenizer
.lastValueIsStillBeingTyped())) {
results.add(new Completion(currentValue.getValue() + suffix, currentValue
.getFormattedValue(), currentValue.getHeading(), currentValue
.getOrder()));
}
}
}
// ROO-389: give inline options given there's multiple choices available and we want to help
// the user
StringBuilder help = new StringBuilder();
help.append(OsUtils.LINE_SEPARATOR);
help.append(option.mandatory() ? "required --" : "optional --");
if ("".equals(option.help())) {
help.append(lastOptionKey).append(": ").append("No help available");
}
else {
help.append(lastOptionKey).append(": ").append(option.help());
}
if (option.specifiedDefaultValue().equals(option.unspecifiedDefaultValue())) {
if (option.specifiedDefaultValue().equals("__NULL__")) {
help.append("; no default value");
}
else {
help.append("; default: '").append(option.specifiedDefaultValue()).append("'");
}
}
else {
if (!"".equals(option.specifiedDefaultValue())
&& !"__NULL__".equals(option.specifiedDefaultValue())) {
help.append("; default if option present: '")
.append(option.specifiedDefaultValue()).append("'");
}
if (!"".equals(option.unspecifiedDefaultValue())
&& !"__NULL__".equals(option.unspecifiedDefaultValue())) {
help.append("; default if option not present: '")
.append(option.unspecifiedDefaultValue()).append("'");
}
}
LOGGER.info(help.toString());
displayHelp(lastOptionKey, option);
if (results.size() == 1) {
String suggestion = results.iterator().next().getValue().trim();
@@ -899,6 +839,56 @@ public class SimpleParser implements Parser {
}
}
private void displayHelp(String lastOptionKey, CliOption option) {
StringBuilder help = new StringBuilder();
help.append(OsUtils.LINE_SEPARATOR);
help.append(option.mandatory() ? "required --" : "optional --");
if ("".equals(option.help())) {
help.append(lastOptionKey).append(": ").append("No help available");
}
else {
help.append(lastOptionKey).append(": ").append(option.help());
}
if (option.specifiedDefaultValue().equals(option.unspecifiedDefaultValue())) {
if (option.specifiedDefaultValue().equals("__NULL__")) {
help.append("; no default value");
}
else {
help.append("; default: '").append(option.specifiedDefaultValue()).append("'");
}
}
else {
if (!"".equals(option.specifiedDefaultValue()) && !"__NULL__".equals(option.specifiedDefaultValue())) {
help.append("; default if option present: '").append(option.specifiedDefaultValue()).append("'");
}
if (!"".equals(option.unspecifiedDefaultValue()) && !"__NULL__".equals(option.unspecifiedDefaultValue())) {
help.append("; default if option not present: '").append(option.unspecifiedDefaultValue()).append("'");
}
}
LOGGER.info(help.toString());
}
private void completeForSimpleTypes(Class<?> parameterType, List<Completion> allValues) {
// Provide some simple options for common types
if (Boolean.class.isAssignableFrom(parameterType) || Boolean.TYPE.isAssignableFrom(parameterType)) {
allValues.add(new Completion("true"));
allValues.add(new Completion("false"));
}
if (Number.class.isAssignableFrom(parameterType)) {
allValues.add(new Completion("0"));
allValues.add(new Completion("1"));
allValues.add(new Completion("2"));
allValues.add(new Completion("3"));
allValues.add(new Completion("4"));
allValues.add(new Completion("5"));
allValues.add(new Completion("6"));
allValues.add(new Completion("7"));
allValues.add(new Completion("8"));
allValues.add(new Completion("9"));
}
}
/**
* populate completion for mandatory options
*

View File

@@ -54,7 +54,7 @@ public class Tokenizer {
/**
* Used to indicate that the last value was indeed half enclosed in quotes. Useful so that parser can re-add it.
*/
private boolean lastValueIsStillBeingTyped;
private boolean openingQuotesHaveNotBeenClosed;
private char lastValueDelimiter;
@@ -137,7 +137,7 @@ public class Tokenizer {
(buffer[pos - 1] != '"' || // quotes are not properly closed
sb.length() == 0)) { // BUT it's ok if consumed nothing (pos-1 is *opening* quote then)
if (allowUnbalancedLastQuotedValue) {
lastValueIsStillBeingTyped = true;
openingQuotesHaveNotBeenClosed = true;
return sb.toString();
}
else {
@@ -166,8 +166,17 @@ public class Tokenizer {
/**
* Return whether the last value was meant to be enclosed in quotes, but the closing quote has not been typed yet.
*/
public boolean lastValueIsStillBeingTyped() {
return lastValueIsStillBeingTyped;
public boolean openingQuotesHaveNotBeenClosed() {
return openingQuotesHaveNotBeenClosed;
}
/**
* Return true if we know for sure that the last value has been typed in full.
*/
public boolean lastValueIsComplete() {
// If using quotes as delim and they're not closed, we know for sure.
// Moreover if we're using space as the delimiter, we can't know.
return !openingQuotesHaveNotBeenClosed && lastValueDelimiter != ' ';
}
/**

View File

@@ -16,7 +16,9 @@
package org.springframework.shell.core;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
@@ -138,6 +140,17 @@ public class SimpleParserTests {
assertThat(candidates, is(empty()));
}
@Test
public void testArgumentValueCompletionAlreadyGiven() {
parser.add(new MyCommands());
parser.add(new StringCompletions(Arrays.asList("abc", "def", "ghi")));
buffer = "bar --option1 def";
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
assertThat(candidates, is(empty()));
}
@Test
public void testDashDashIntoOption() {
parser.add(new MyCommands());
@@ -206,6 +219,27 @@ public class SimpleParserTests {
}
@Test
public void testWithDefaultKey() {
parser.add(new MyCommands());
buffer = "testDefaultKey thevalue --optio";
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
assertThat(candidates, hasItem(completionThat(is(equalTo("testDefaultKey thevalue --option2 ")))));
assertThat(candidates, hasItem(completionThat(is(equalTo("testDefaultKey thevalue --option3 ")))));
assertThat(candidates, not(hasItem(completionThat(containsString("option1")))));
// Let's do it again with default key in the middle
candidates.clear();
buffer = "testDefaultKey --option3 foo thevalue --optio";
offset = parser.completeAdvanced(buffer, buffer.length(), candidates);
assertThat(candidates, hasItem(completionThat(is(equalTo("testDefaultKey --option3 foo thevalue --option2 ")))));
assertThat(candidates, not(hasItem(completionThat(endsWith("option3 ")))));
}
@Test
public void testStopAtFirstWhenMandatory() {
parser.add(new MyCommands());
@@ -290,6 +324,14 @@ public class SimpleParserTests {
String option3) {
}
@CliCommand("testDefaultKey")
public void testDefaultKey(@CliOption(key = "")
String option1, @CliOption(key = "option2")
String option2, @CliOption(key = "option3")
String option3) {
}
}
public static class StringCompletions implements Converter<String> {

View File

@@ -132,7 +132,7 @@ public class TokenizerTests {
expected.put("foo", "bar bazz");
expected.put("bizz", "unfinished bizness ");
assertEquals(expected, result);
assertTrue(tokenizer.lastValueIsStillBeingTyped());
assertTrue(tokenizer.openingQuotesHaveNotBeenClosed());
}
@Test
@@ -143,7 +143,7 @@ public class TokenizerTests {
expected.put("foo", "bar bazz");
expected.put("bizz", "");
assertEquals(expected, result);
assertTrue(tokenizer.lastValueIsStillBeingTyped());
assertTrue(tokenizer.openingQuotesHaveNotBeenClosed());
}
@Test