diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArguments.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArguments.java index 9b7135f04..248a6c495 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArguments.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArguments.java @@ -66,6 +66,14 @@ public void stopSplitShortOptions() { this.splitShortOpts = false; } + /** + * @return {@code true} if short options (e.g. "-bdf") should not be split (e.g. into "-b -d -f" for "--batch --debug --force"), {@code false} otherwise. + */ + public boolean isSplitShortOpts() { + + return splitShortOpts; + } + /** * @return {@code true} if the options have ended, {@code false} otherwise. * @see CliArgument#isEndOptions() @@ -87,6 +95,21 @@ private void setCurrent(CliArgument arg) { } } + /** + * @return {@code true} if the last argument shall be {@link CliArgument#isCompletion() completed}, {@code false}. + */ + public boolean isCompletion() { + + CliArgument arg = this.currentArg; + while ((arg != null) && !arg.isEnd()) { + if (arg.isCompletion()) { + return true; + } + arg = arg.next; + } + return false; + } + /** * @return the initial {@link CliArgument}. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java index 3001da170..158243593 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java @@ -73,7 +73,7 @@ public List> getValues() { /** * Clear the set values on all properties of the {@link Commandlet#propertiesList} */ - public void clearProperties() { + public void reset() { for (Property property : this.propertiesList) { property.clearValue(); @@ -94,10 +94,19 @@ public Property getOption(String nameOrAlias) { */ protected void addKeyword(String keyword) { + addKeyword(keyword, null); + } + + /** + * @param keyword the {@link KeywordProperty keyword} to {@link #add(Property) add}. + * @param alias the optional {@link KeywordProperty#getAlias() alias}. + */ + protected void addKeyword(String keyword, String alias) { + if (this.properties.isEmpty()) { this.firstKeyword = keyword; } - add(new KeywordProperty(keyword, true, null)); + add(new KeywordProperty(keyword, true, alias)); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java index eb40903e0..92105a33e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java @@ -1,7 +1,10 @@ package com.devonfw.tools.ide.commandlet; import java.util.Collection; +import java.util.Iterator; +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.property.KeywordProperty; import com.devonfw.tools.ide.property.Property; import com.devonfw.tools.ide.tool.LocalToolCommandlet; @@ -92,4 +95,15 @@ default LocalToolCommandlet getRequiredLocalToolCommandlet(String name) { throw new IllegalArgumentException("The commandlet " + name + " is not a LocalToolCommandlet!"); } + /** + * @param arguments the {@link CliArguments}. + * @param collector the optional {@link CompletionCandidateCollector}. Will be {@code null} if no argument {@link CliArguments#isCompletion() completion} + * shall be performed. + * @return an {@link Iterator} of the matching {@link Commandlet}(s). Typically empty or containing a single {@link Commandlet}. Only in edge-cases multiple + * {@link Commandlet}s could be found (e.g. if two {@link Commandlet}s exist with the same keyword but with different mandatory properties such as in our + * legacy devonfw-ide "ide get version ..." and "ide get edition ..." - however, we redesigned our CLI to "ide get-version ..." and "ide get-edition ..." + * to simplify this). + */ + Iterator findCommandlet(CliArguments arguments, CompletionCandidateCollector collector); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index a1ab0b4d9..e4c200fbb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -3,9 +3,14 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import com.devonfw.tools.ide.cli.CliArgument; +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.KeywordProperty; import com.devonfw.tools.ide.property.Property; @@ -44,6 +49,8 @@ */ public class CommandletManagerImpl implements CommandletManager { + private final IdeContext context; + private final Map, Commandlet> commandletTypeMap; private final Map commandletNameMap; @@ -60,6 +67,7 @@ public class CommandletManagerImpl implements CommandletManager { public CommandletManagerImpl(IdeContext context) { super(); + this.context = context; this.commandletTypeMap = new HashMap<>(); this.commandletNameMap = new HashMap<>(); this.firstKeywordMap = new HashMap<>(); @@ -123,14 +131,14 @@ protected void add(Commandlet commandlet) { boolean hasRequiredProperty = false; List> properties = commandlet.getProperties(); int propertyCount = properties.size(); + String keyword = commandlet.getKeyword(); + if (keyword != null) { + this.firstKeywordMap.putIfAbsent(keyword, commandlet); + } for (int i = 0; i < propertyCount; i++) { Property property = properties.get(i); if (property.isRequired()) { hasRequiredProperty = true; - if ((i == 0) && (property instanceof KeywordProperty)) { - String keyword = property.getName(); - this.firstKeywordMap.putIfAbsent(keyword, commandlet); - } break; } } @@ -163,8 +171,7 @@ public C getCommandlet(Class commandletType) { @Override public Commandlet getCommandlet(String name) { - Commandlet commandlet = this.commandletNameMap.get(name); - return commandlet; + return this.commandletNameMap.get(name); } @Override @@ -173,4 +180,90 @@ public Commandlet getCommandletByFirstKeyword(String keyword) { return this.firstKeywordMap.get(keyword); } + @Override + public Iterator findCommandlet(CliArguments arguments, CompletionCandidateCollector collector) { + + CliArgument current = arguments.current(); + if (current.isEnd()) { + return Collections.emptyIterator(); + } + String keyword = current.get(); + Commandlet commandlet = getCommandletByFirstKeyword(keyword); + if ((commandlet == null) && (collector == null)) { + return Collections.emptyIterator(); + } + return new CommandletFinder(commandlet, arguments.copy(), collector); + } + + private final class CommandletFinder implements Iterator { + + private final Commandlet firstCandidate; + + private final Iterator commandletIterator; + + private final CliArguments arguments; + + private final CompletionCandidateCollector collector; + + private Commandlet next; + + private CommandletFinder(Commandlet firstCandidate, CliArguments arguments, CompletionCandidateCollector collector) { + + this.firstCandidate = firstCandidate; + this.commandletIterator = getCommandlets().iterator(); + this.arguments = arguments; + this.collector = collector; + if (isSuitable(firstCandidate)) { + this.next = firstCandidate; + } else { + this.next = findNext(); + } + } + + @Override + public boolean hasNext() { + + return this.next != null; + } + + @Override + public Commandlet next() { + + if (this.next == null) { + throw new NoSuchElementException(); + } + Commandlet result = this.next; + this.next = findNext(); + return result; + } + + private boolean isSuitable(Commandlet commandlet) { + + return (commandlet != null) && (!commandlet.isIdeHomeRequired() || (context.getIdeHome() != null)); + } + + private Commandlet findNext() { + while (this.commandletIterator.hasNext()) { + Commandlet cmd = this.commandletIterator.next(); + if ((cmd != this.firstCandidate) && isSuitable(cmd)) { + List> properties = cmd.getProperties(); + // validation should already be done in add method and could be removed here... + if (properties.isEmpty()) { + assert false : cmd.getClass().getSimpleName() + " has no properties!"; + } else { + Property property = properties.get(0); + if (property instanceof KeywordProperty) { + boolean matches = property.apply(arguments.copy(), context, cmd, this.collector); + if (matches) { + return cmd; + } + } else { + assert false : cmd.getClass().getSimpleName() + " is invalid as first property must be keyword property but is " + property; + } + } + } + } + return null; + } + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionCommandlet.java index 2aaa3fc97..3a1a7cc8a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionCommandlet.java @@ -2,7 +2,6 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.version.IdeVersion; /** @@ -18,7 +17,7 @@ public class VersionCommandlet extends Commandlet { public VersionCommandlet(IdeContext context) { super(context); - addKeyword(new FlagProperty(getName(), true, "-v")); + addKeyword(getName(), "-v"); } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java b/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java index 4f2edeb8f..3051cf309 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java @@ -32,7 +32,7 @@ public IdeCompleter(AbstractIdeContext context) { public void complete(LineReader reader, ParsedLine commandLine, List candidates) { List words = commandLine.words(); CliArguments args = CliArguments.ofCompletion(words.toArray(String[]::new)); - List completion = this.context.complete(args, false); + List completion = this.context.complete(args, true); int i = 0; for (CompletionCandidate candidate : completion) { candidates.add(new Candidate(candidate.text(), candidate.text(), null, null, null, null, true, i++)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 49a47bc11..84454e540 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -6,6 +6,7 @@ import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -788,39 +789,27 @@ public int run(CliArguments arguments) { assert (this.currentStep == null); boolean supressStepSuccess = false; StepImpl step = newStep(true, "ide", (Object[]) current.asArray()); - Commandlet firstCandidate = null; + Iterator commandletIterator = this.commandletManager.findCommandlet(arguments, null); + Commandlet cmd = null; + ValidationResult result = null; try { - if (!current.isEnd()) { - String keyword = current.get(); - firstCandidate = this.commandletManager.getCommandletByFirstKeyword(keyword); - ValidationResult firstResult = null; - if (firstCandidate != null) { - firstResult = applyAndRun(arguments.copy(), firstCandidate); - if (firstResult.isValid()) { - supressStepSuccess = firstCandidate.isSuppressStepSuccess(); - step.success(); - return ProcessResult.SUCCESS; - } - } - for (Commandlet cmd : this.commandletManager.getCommandlets()) { - if (cmd != firstCandidate) { - ValidationResult result = applyAndRun(arguments.copy(), cmd); - if (result.isValid()) { - supressStepSuccess = cmd.isSuppressStepSuccess(); - step.success(); - return ProcessResult.SUCCESS; - } - } - } - if (firstResult != null) { - throw new CliException(firstResult.getErrorMessage()); + while (commandletIterator.hasNext()) { + cmd = commandletIterator.next(); + result = applyAndRun(arguments.copy(), cmd); + if (result.isValid()) { + supressStepSuccess = cmd.isSuppressStepSuccess(); + step.success(); + return ProcessResult.SUCCESS; } - step.error("Invalid arguments: {}", current.getArgs()); } + if (result != null) { + error(result.getErrorMessage()); + } + step.error("Invalid arguments: {}", current.getArgs()); HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class); - if (firstCandidate != null) { - help.commandlet.setValue(firstCandidate); + if (cmd != null) { + help.commandlet.setValue(cmd); } help.run(); return 1; @@ -835,16 +824,15 @@ public int run(CliArguments arguments) { } /** - * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet, CompletionCandidateCollector) apply} and - * {@link Commandlet#run() run}. + * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}. * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the * {@link Commandlet} did not match and we have to try a different candidate). */ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) { - cmd.clearProperties(); - - ValidationResult result = apply(arguments, cmd, null); + IdeLogLevel previousLogLevel = null; + cmd.reset(); + ValidationResult result = apply(arguments, cmd); if (result.isValid()) { result = cmd.validate(); } @@ -855,53 +843,58 @@ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) { } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) { throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT); } - if (cmd.isProcessableOutput()) { - if (!debug().isEnabled()) { - // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere - for (IdeLogLevel level : IdeLogLevel.values()) { - if (level != IdeLogLevel.PROCESSABLE) { - this.startContext.setLogLevel(level, false); - } + try { + if (cmd.isProcessableOutput()) { + if (!debug().isEnabled()) { + // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere + previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE); } - } - this.startContext.activateLogging(); - } else { - this.startContext.activateLogging(); - if (!isTest()) { - if (this.ideRoot == null) { - warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again."); - } else if (this.ideHome != null) { - Path ideRootPath = getIdeRootPathFromEnv(); - if (!this.ideRoot.equals(ideRootPath)) { - warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath, - this.ideHome.getFileName(), this.ideRoot); + this.startContext.activateLogging(); + } else { + this.startContext.activateLogging(); + verifyIdeRoot(); + if (cmd.isIdeHomeRequired()) { + debug(getMessageIdeHomeFound()); + } + if (this.settingsPath != null) { + if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || + (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { + interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); } } } - if (cmd.isIdeHomeRequired()) { - debug(getMessageIdeHomeFound()); - } - if (this.settingsPath != null) { - if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || - (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { - interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); - } + cmd.run(); + } finally { + if (previousLogLevel != null) { + this.startContext.setLogLevel(previousLogLevel); } } - cmd.run(); } else { trace("Commandlet did not match"); } return result; } + private void verifyIdeRoot() { + if (!isTest()) { + if (this.ideRoot == null) { + warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again."); + } else if (this.ideHome != null) { + Path ideRootPath = getIdeRootPathFromEnv(); + if (!this.ideRoot.equals(ideRootPath)) { + warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath, + this.ideHome.getFileName(), this.ideRoot); + } + } + } + } + /** * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}. * @param includeContextOptions to include the options of {@link ContextCommandlet}. * @return the {@link List} of {@link CompletionCandidate}s to suggest. */ public List complete(CliArguments arguments, boolean includeContextOptions) { - CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this); if (arguments.current().isStart()) { arguments.next(); @@ -913,32 +906,75 @@ public List complete(CliArguments arguments, boolean includ property.apply(arguments, this, cc, collector); } } + Iterator commandletIterator = this.commandletManager.findCommandlet(arguments, collector); CliArgument current = arguments.current(); - if (!current.isEnd()) { - String keyword = current.get(); - Commandlet firstCandidate = this.commandletManager.getCommandletByFirstKeyword(keyword); - boolean matches = false; - if (firstCandidate != null) { - matches = completeCommandlet(arguments, firstCandidate, collector); - } else if (current.isCombinedShortOption()) { - collector.add(keyword, null, null, null); - } - if (!matches) { - for (Commandlet cmd : this.commandletManager.getCommandlets()) { - if (cmd != firstCandidate) { - completeCommandlet(arguments, cmd, collector); - } - } + if (current.isCompletion() && current.isCombinedShortOption()) { + collector.add(current.get(), null, null, null); + } + arguments.next(); + while (commandletIterator.hasNext()) { + Commandlet cmd = commandletIterator.next(); + if (!arguments.current().isEnd()) { + completeCommandlet(arguments.copy(), cmd, collector); } } return collector.getSortedCandidates(); } - private boolean completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) { - if (cmd.isIdeHomeRequired() && (this.ideHome == null)) { - return false; - } else { - return apply(arguments.copy(), cmd, collector).isValid(); + private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) { + trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName()); + Iterator> valueIterator = cmd.getValues().iterator(); + valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet + List> properties = cmd.getProperties(); + // we are creating our own list of options and remove them when matched to avoid duplicate suggestions + List> optionProperties = new ArrayList<>(properties.size()); + for (Property property : properties) { + if (property.isOption()) { + optionProperties.add(property); + } + } + CliArgument currentArgument = arguments.current(); + while (!currentArgument.isEnd()) { + trace("Trying to match argument '{}'", currentArgument); + if (currentArgument.isOption() && !arguments.isEndOptions()) { + if (currentArgument.isCompletion()) { + Iterator> optionIterator = optionProperties.iterator(); + while (optionIterator.hasNext()) { + Property option = optionIterator.next(); + boolean success = option.apply(arguments, this, cmd, collector); + if (success) { + optionIterator.remove(); + arguments.next(); + } + } + } else { + Property option = cmd.getOption(currentArgument.get()); + if (option != null) { + arguments.next(); + boolean removed = optionProperties.remove(option); + if (!removed) { + option = null; + } + } + if (option == null) { + trace("No such option was found."); + return; + } + } + } else { + if (valueIterator.hasNext()) { + Property valueProperty = valueIterator.next(); + boolean success = valueProperty.apply(arguments, this, cmd, collector); + if (!success) { + trace("Completion cannot match any further."); + return; + } + } else { + trace("No value left for completion."); + return; + } + } + currentArgument = arguments.current(); } } @@ -947,20 +983,13 @@ private boolean completeCommandlet(CliArguments arguments, Commandlet cmd, Compl * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a * {@link CliArguments#copy() copy} as needed. * @param cmd the potential {@link Commandlet} to match. - * @param collector the {@link CompletionCandidateCollector}. - * @return {@code true} if the given {@link Commandlet} matches to the given {@link CliArgument}(s) and those have been applied (set in the {@link Commandlet} - * and {@link Commandlet#validate() validated}), {@code false} otherwise (the {@link Commandlet} did not match and we have to try a different candidate). + * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred. */ - public ValidationResult apply(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) { + public ValidationResult apply(CliArguments arguments, Commandlet cmd) { trace("Trying to match arguments to commandlet {}", cmd.getName()); CliArgument currentArgument = arguments.current(); - Iterator> propertyIterator; - if (currentArgument.isCompletion()) { - propertyIterator = cmd.getProperties().iterator(); - } else { - propertyIterator = cmd.getValues().iterator(); - } + Iterator> propertyIterator = cmd.getValues().iterator(); Property property = null; if (propertyIterator.hasNext()) { property = propertyIterator.next(); @@ -993,8 +1022,8 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd, Completion arguments.stopSplitShortOptions(); } } - boolean matches = currentProperty.apply(arguments, this, cmd, collector); - if (!matches || currentArgument.isCompletion()) { + boolean matches = currentProperty.apply(arguments, this, cmd, null); + if (!matches && currentArgument.isCompletion()) { ValidationState state = new ValidationState(null); state.addErrorMessage("No matching property found"); return state; diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java index b63bbed10..b0bd8fba4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java @@ -27,6 +27,9 @@ public boolean onLog(IdeLogLevel level, String message, String rawMessage, Objec */ public void flushAndDisable(IdeLogger logger) { + if (this.entries == null) { + return; + } List buffer = this.entries; // disable ourselves from collecting further events this.entries = null; diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java index bcc85dc11..d8f1a166b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java @@ -44,13 +44,22 @@ public IdeSubLogger level(IdeLogLevel level) { * Sets the log level. * * @param logLevel {@link IdeLogLevel} + * @return the previous set logLevel {@link IdeLogLevel} */ - public void setLogLevel(IdeLogLevel logLevel) { + public IdeLogLevel setLogLevel(IdeLogLevel logLevel) { + IdeLogLevel previousLogLevel = null; for (IdeLogLevel level : IdeLogLevel.values()) { boolean enabled = level.ordinal() >= logLevel.ordinal(); + if ((previousLogLevel == null) && this.loggers[level.ordinal()].isEnabled()) { + previousLogLevel = level; + } setLogLevel(level, enabled); } + if ((previousLogLevel == null) || (previousLogLevel.ordinal() > IdeLogLevel.INFO.ordinal())) { + previousLogLevel = IdeLogLevel.INFO; + } + return previousLogLevel; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java index f3cf0a3a1..74d0574ad 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java @@ -20,7 +20,13 @@ public class KeywordProperty extends BooleanProperty { public KeywordProperty(String name, boolean required, String alias) { super(name, required, alias); - assert (!name.isEmpty() && isValue()); + assert !name.isEmpty(); + } + + @Override + public boolean isValue() { + + return true; } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java index b96effa7b..16ed1f71a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java @@ -85,7 +85,7 @@ public Property(String name, boolean required, String alias, boolean multivalued } /** - * @return the name of this property. + * @return the name of this property. Will be the empty {@link String} for a {@link #isValue() value} property that is not a keyword. */ public String getName() { @@ -335,14 +335,15 @@ public boolean apply(CliArguments args, IdeContext context, Commandlet commandle if (argument.isCompletion()) { int size = collector.getCandidates().size(); complete(argument, args, context, commandlet, collector); - if (collector.getCandidates().size() > size) { // completions added so complete matched? - return true; - } + return (collector.getCandidates().size() > size); } boolean option = isOption(); if (option && !argument.isOption()) { return false; } + if (!option && argument.isOption() && args.isSplitShortOpts()) { + return false; + } String argValue = null; boolean lookahead = false; if (this.name.isEmpty()) { @@ -384,7 +385,6 @@ protected boolean applyValue(String argValue, boolean lookahead, CliArguments ar if (success) { if (this.multivalued) { - while (success && args.hasNext()) { CliArgument arg = args.next(); success = assignValueAsString(arg.get(), context, commandlet); diff --git a/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java b/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java index 828e9e84c..087cfbc69 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java @@ -35,6 +35,23 @@ public void testCompleteEmpty() { .containsExactly(expectedCandidates.toArray(String[]::new)); } + /** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for long option. */ + @Test + public void testCompleteLongOptionBatch() { + + // arrange + boolean includeContextOptions = true; + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + CliArguments args = CliArguments.ofCompletion("--b"); + args.next(); + List expectedCandidates = List.of("--batch"); + // act + List candidates = context.complete(args, includeContextOptions); + // assert + assertThat(candidates.stream().map(CompletionCandidate::text)) + .containsExactly(expectedCandidates.toArray(String[]::new)); + } + /** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for empty input. */ @Test public void testCompleteEmptyNoCtxOptions() { @@ -120,6 +137,19 @@ public void testCompleteVersionNoMoreArgs() { assertThat(candidates).isEmpty(); } + /** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for an option inside a commandlet. */ + @Test + public void testCompleteCommandletOption() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + CliArguments args = CliArguments.ofCompletion("get-version", "--c"); + // act + List candidates = context.complete(args, true); + // assert + assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("--configured"); + } + private static List getExpectedCandidates(AbstractIdeContext context, boolean commandlets, boolean ctxOptions, boolean addVersionAlias) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java index a979f8db4..eef53376c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java @@ -31,6 +31,17 @@ public void testIdeCompleterVersion() { assertBuffer("--version ", new TestBuffer("--vers").tab()); } + /** + * Test of 1st level auto-completion (commandlet name). Here we test the special case of the {@link com.devonfw.tools.ide.commandlet.VersionCommandlet} that + * has a long-option style. + */ + @Test + public void testIdeCompleterBatch() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("--batch ", new TestBuffer("--b").tab()); + } + /** * Test of 2nd level auto-completion with tool property of {@link com.devonfw.tools.ide.commandlet.InstallCommandlet}. */ @@ -92,7 +103,7 @@ public void testIdeCompleterNonExistentCommand() { public void testIdeCompleterPreventsOptionsAfterCommandWithMinus() { this.reader.setCompleter(newCompleter()); - assertBuffer("get-version -", new TestBuffer("get-version -").tab().tab()); + assertBuffer("get-version --configured ", new TestBuffer("get-version -").tab().tab()); assertBuffer("get-version - ", new TestBuffer("get-version - ").tab().tab()); } @@ -119,6 +130,26 @@ public void testIdeCompleterHandlesOptionsBeforeCommand() { assertBuffer("get-version mvn ", new TestBuffer("get-version mv").tab().tab()); } + /** + * Test of completion of options after commandlets. + */ + @Test + public void testIdeCompleterWithOptionAfterCommandletWorks() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("env --bash ", new TestBuffer("env --ba").tab().tab()); + } + + /** + * Test of completion of options and arguments after commandlets. + */ + @Test + public void testIdeCompleterWithOptionAndArguments() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("get-version --configured ", new TestBuffer("get-version --c").tab().tab()); + } + private IdeCompleter newCompleter() { return new IdeCompleter(newTestContext()); diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java index 2d283b863..de3c1d66f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java @@ -134,7 +134,7 @@ protected AbstractEnvironmentVariables createSystemVariables() { public IdeSystemTestImpl getSystem() { if (this.system == null) { - this.system = IdeSystemTestImpl.ofSystemDefaults(this); + this.system = new IdeSystemTestImpl(this); } return (IdeSystemTestImpl) this.system; }