From 1ebcaf54fbfbb7022eb45124cfc47e000a2a4836 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 10:49:26 +0200 Subject: [PATCH 01/14] Add completion tests --- .../ide/commandlet/CommandletManagerTest.java | 64 +++++++++ .../ide/commandlet/ShellCommandletTest.java | 33 +++++ .../ide/context/AbstractIdeContextTest.java | 20 +++ .../tools/ide/property/PropertyTest.java | 121 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/CommandletManagerTest.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/ShellCommandletTest.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/property/PropertyTest.java diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CommandletManagerTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CommandletManagerTest.java new file mode 100644 index 0000000000..6fa7a0eaa9 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CommandletManagerTest.java @@ -0,0 +1,64 @@ +package com.devonfw.tools.ide.commandlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.CompletionCandidate; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; +import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault; +import com.devonfw.tools.ide.context.AbstractIdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import org.junit.jupiter.api.Test; + +public class CommandletManagerTest { + @Test + public void testMvnCommandletIsPartOfCommandletIterator() { + IdeTestContext context = new IdeTestContext(); + CommandletManager manager = context.getCommandletManager(); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + assertThat(manager.findCommandlet(new CliArguments("mvn"), collector).hasNext()).isTrue(); + } + + @Test + public void testCollectCompletionCandidatesEmptyInputReturnsAllCommandlets() { + IdeTestContext context = new IdeTestContext(); + CommandletManager manager = context.getCommandletManager(); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(context); + + manager.collectCompletionCandidates(CliArguments.ofCompletion(""), collector); + + List lines = collector.getSortedCandidates().stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).contains("complete", "create", "env", "install", "shell", "status", "update", + "mvn"); + } + + @Test + public void testCollectCompletionCandidatesPartialInputFiltersToMatches() { + IdeTestContext context = new IdeTestContext(); + CommandletManager manager = context.getCommandletManager(); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(context); + + manager.collectCompletionCandidates(CliArguments.ofCompletion("ins"), collector); + + List lines = collector.getSortedCandidates().stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("install"); + } + + @Test + public void testCollectCompletionCandidatesNoMatchReturnsEmpty() { + IdeTestContext context = new IdeTestContext(); + CommandletManager manager = context.getCommandletManager(); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(context); + + manager.collectCompletionCandidates(CliArguments.ofCompletion("xyz"), collector); + + assertThat(collector.getSortedCandidates()).isEmpty(); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ShellCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ShellCommandletTest.java new file mode 100644 index 0000000000..08dbeb8f1f --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ShellCommandletTest.java @@ -0,0 +1,33 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.CompletionCandidate; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Test of {@link ShellCommandlet}. + */ +class ShellCommandletTest extends AbstractIdeContextTest { + @Test + public void testExitIsOfferedAsCompletion() { + IdeTestContext context = new IdeTestContext(); + CliArguments args = CliArguments.of(0, ""); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).contains("exit"); + } + + @Test + public void testExitIsOfferedOnPartialInput() { + IdeTestContext context = new IdeTestContext(); + CliArguments args = CliArguments.of(0, "ex"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("exit"); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java index ad5d37022a..5bc3a6329f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java @@ -12,7 +12,12 @@ import java.util.Set; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; +import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileCopyMode; import com.devonfw.tools.ide.io.IdeProgressBarTestImpl; @@ -319,4 +324,19 @@ protected void verifyStartScript(IdeContext context, String ide, String workspac } } + @Test + public void testMvIsCompletedToMvn() { + AbstractIdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("mv"); + + assertThat(context.complete(args, true).getFirst().text()).isEqualTo("mvn"); + } + + @Test + public void testMvIsCompletedToMvnWithoutContextCompletion() { + AbstractIdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("mv"); + + assertThat(context.complete(args, false).getFirst().text()).isEqualTo("mvn"); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/property/PropertyTest.java b/cli/src/test/java/com/devonfw/tools/ide/property/PropertyTest.java new file mode 100644 index 0000000000..49f99a06d6 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/property/PropertyTest.java @@ -0,0 +1,121 @@ +package com.devonfw.tools.ide.property; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.commandlet.ContextCommandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; +import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for class `Property`. + */ +public class PropertyTest { + @Test + public void testApplyAddsPropertyAsCompletionCandidate() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("--for"); + + Commandlet ctxCmd = new ContextCommandlet(); + Property flagProperty = new FlagProperty("--force"); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + flagProperty.apply("--force", args, context, ctxCmd, collector); + + assertThat(collector.getSortedCandidates().getFirst().text()).isEqualTo("--force"); + } + + @Test + public void testApplyWithCliArgumentAddsPropertyAsCompletionCandidate() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("mv"); + + Commandlet mvnCmd = context.getCommandletManager().getCommandletByFirstKeyword("mvn"); + Property keywordProperty = new KeywordProperty("mvn", false, ""); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + keywordProperty.apply(args, context, mvnCmd, collector); + + assertThat(collector.getSortedCandidates().getFirst().text()).isEqualTo("mvn"); + } + + @Test + public void testMismatchingApplyReturnsFalse() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("mv"); + + Commandlet mvnCmd = context.getCommandletManager().getCommandletByFirstKeyword("mvn"); + Property flagProperty = new FlagProperty("--force"); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + assertThat(flagProperty.apply(args, context, mvnCmd, collector)).isFalse(); + } + + @Test + public void testMismatchingApplyOnKeywordReturnsFalse() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("st"); + + Commandlet mvnCmd = context.getCommandletManager().getCommandletByFirstKeyword("mvn"); + Property keywordProperty = new KeywordProperty("mvn", false, ""); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + assertThat(keywordProperty.apply(args, context, mvnCmd, collector)).isFalse(); + } + + @Test + public void testCompleteAddsToCandidatesOnMatch() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("--for"); + + Commandlet ctxCmd = context.getCommandletManager().getCommandletByFirstKeyword("context"); + Property flagProperty = new FlagProperty("--force"); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + flagProperty.complete("--force", args.current(), args, context, ctxCmd, collector); + + assertThat(collector.getSortedCandidates().getFirst().text()).isEqualTo("--force"); + } + + @Test + public void testCompleteAddsCandidateForNonOption() { + IdeContext context = new IdeTestContext(); + CliArguments args = CliArguments.ofCompletion("mv"); + + Commandlet ctxCmd = context.getCommandletManager().getCommandletByFirstKeyword("mvn"); + Property keywordProperty = new KeywordProperty("mvn", false, ""); + CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault( + context + ); + + args.next(); + + keywordProperty.complete("mvn", args.current(), args, context, ctxCmd, collector); + + assertThat(collector.getSortedCandidates().getFirst().text()).isEqualTo("mvn"); + } +} From 608c40c848fe71502de97bd19a1fcfd79d59bc50 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 10:51:03 +0200 Subject: [PATCH 02/14] Allow selection of completion argument in CliArguments --- .../java/com/devonfw/tools/ide/cli/CliArgument.java | 13 +++++++++++++ .../com/devonfw/tools/ide/cli/CliArguments.java | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java index d91efa30cb..58bce2a9f6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java @@ -273,6 +273,19 @@ public String toString() { return this.arg; } + public static CliArgument of(int completionIdx, String... args) { + CliArgument current = END; + int last = args.length - 1; + + for (int argsIdx = last; argsIdx >= 0; argsIdx--) { + String arg = args[argsIdx]; + boolean completionArg = argsIdx == completionIdx; + + current = new CliArgument(arg, current, completionArg); + } + + return current.createStart(); + } /** * @param args the command-line arguments (e.g. from {@code main} method). * @return the first {@link CliArgument} of the parsed arguments or {@code null} if for empty arguments. 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 248a6c495c..a272644072 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 @@ -15,6 +15,10 @@ public class CliArguments implements Iterator { private boolean splitShortOpts; + public CliArguments(int completionIdx, String... args) { + this(CliArgument.of(completionIdx, args)); + } + /** * The constructor. * @@ -165,6 +169,10 @@ public String toString() { return this.currentArg.getArgs(); } + public static CliArguments of(int completionIdx, String... args) { + return new CliArguments(completionIdx, args); + } + /** * @param args the {@link CliArgument#of(String...) command line arguments}. * @return the {@link CliArguments}. From 2daba5d5eaf86b2dc6d148e6b0cc8a43df5f8d20 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 10:52:01 +0200 Subject: [PATCH 03/14] Add more completion tests --- .../devonfw/tools/ide/tool/mvn/MvnTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnTest.java index 7cf8e2d121..735c69d060 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnTest.java @@ -4,12 +4,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.CompletionCandidate; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.io.FileAccess; @@ -138,4 +141,126 @@ private void assertFileContent(Path filePath, List expectedValues) throw assertThat(values).containsExactlyInAnyOrderElementsOf(expectedValues); } + + @Test + public void testEmptyGoalInputSuggestsAllMavenProperties() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", ""); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).contains("clean", "compile", "test", "package", "install", "deploy"); + } + + @Test + public void testEmptyInputSuggestsFlagProperties() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", ""); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).contains("--also-make", "--also-make-dependents", "--fail-at-end", + "--fail-fast", "--threads", "-DskipTests", "-DdeployAtEnd"); + } + + @Test + public void testPartialGoalInputFiltersToMatchingGoals() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", "cl"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("clean"); + } + + @Test + public void testUnmatchedGoalInputReturnsNoSuggestions() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", "xyz"); + + assertThat(context.complete(args, false)).isEmpty(); + } + + @Test + public void testSecondGoalIsCompletedAfterFirstGoal() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(2, "mvn", "clean", ""); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).contains("package", "install", "deploy", "test"); + } + + @Test + public void testSecondGoalPartialInputFiltersToMatches() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(2, "mvn", "clean", "pa"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("package"); + } + + @Test + public void testAlsoMakeFlagIsOfferedOnPartialInput() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", "--also"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("--also-make", "--also-make-dependents"); + } + + @Test + public void testShortAlsoMakeFlagIsOfferedOnPartialInput() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", "-am"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).containsExactly("-am", "-amd"); + } + + @Test + public void testExecMainClassFlagCandidateIsMarkedIncomplete() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(1, "mvn", "-Dexec.mainClass="); + + Optional candidate = context.complete(args, false).stream() + .filter(c -> c.text().equals("-Dexec.mainClass=")) + .findFirst(); + + assertThat(candidate).isPresent(); + assertThat(candidate.get().complete()).isFalse(); + } + + @Test + public void testAlsoMakeFlagIsNotOfferedWhenAlreadyUsed() { + String path = "project/workspaces"; + + IdeTestContext context = newContext(PROJECT_MVN, path, false); + CliArguments args = CliArguments.of(2, "mvn", "--also-make", "--al"); + + List lines = context.complete(args, false).stream() + .map(CompletionCandidate::text).toList(); + assertThat(lines).doesNotContain("--also-make"); + } } From dac795f2b7656b296ff84299ddfeb4b8906532d1 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 10:55:00 +0200 Subject: [PATCH 04/14] Extract completion candidate collection into own method --- .../ide/commandlet/CommandletManager.java | 3 +++ .../ide/commandlet/CommandletManagerImpl.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) 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 92105a33ea..813efdd59f 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 @@ -95,6 +95,9 @@ default LocalToolCommandlet getRequiredLocalToolCommandlet(String name) { throw new IllegalArgumentException("The commandlet " + name + " is not a LocalToolCommandlet!"); } + public abstract void collectCompletionCandidates(CliArguments arguments, + CompletionCandidateCollector collector); + /** * @param arguments the {@link CliArguments}. * @param collector the optional {@link CompletionCandidateCollector}. Will be {@code null} if no argument {@link CliArguments#isCompletion() completion} 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 9d4217fb8b..e4dfc63acb 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 @@ -226,6 +226,28 @@ public Commandlet getCommandletByFirstKeyword(String keyword) { return this.firstKeywordMap.get(keyword); } + @Override + public void collectCompletionCandidates(CliArguments arguments, + CompletionCandidateCollector collector) { + CliArgument current = arguments.current(); + if (current.isStart()) { + current = current.getNext(); + } + if (current.isEnd()) { + return; + } + + for (Commandlet cmd : this.getCommandlets()) { + if (!cmd.isIdeHomeRequired() || this.context.getIdeHome() != null) { + for (Property property : cmd.getProperties()) { + if (property instanceof KeywordProperty keyword) { + keyword.apply(arguments, this.context, cmd, collector); + } + } + } + } + } + @Override public Iterator findCommandlet(CliArguments arguments, CompletionCandidateCollector collector) { From f63af46131bd1db0e260574aacfc928da50564d2 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:01:19 +0200 Subject: [PATCH 05/14] Change CompletionCandidate test to entries list --- .../tools/ide/completion/CompletionCandidate.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java index c62bd07d84..c58fe20908 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java @@ -1,16 +1,24 @@ package com.devonfw.tools.ide.completion; +import java.util.List; + /** * Candidate for auto-completion. * * @param text the text to suggest (CLI argument value). * @param description the description of the candidate. */ -public record CompletionCandidate(String text, String description) implements Comparable { +public record CompletionCandidate(List entries, + String description, + boolean complete) implements Comparable { @Override public int compareTo(CompletionCandidate o) { - return this.text.compareTo(o.text); + return this.text().compareTo(o.text()); + } + + public String text() { + return String.join(" ", this.entries); } } From 682226cbe3ccc48143063a6ad6cc9b3f75afa1fc Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:04:17 +0200 Subject: [PATCH 06/14] Adjust constructor call --- .../tools/ide/completion/CompletionCandidateCollector.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java index 2a9e3c7161..258b87b6d2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java @@ -27,12 +27,14 @@ public interface CompletionCandidateCollector { * @param commandlet the {@link Commandlet} owning the {@link Property}. * @return the {@link CompletionCandidate} for the given parameters. */ - default CompletionCandidate createCandidate(String text, String description, Property property, Commandlet commandlet) { + default CompletionCandidate createCandidate(String text, String description, + Property property, Commandlet commandlet, boolean complete) { if (description == null) { // compute description from property + commandlet like in HelpCommandlet? } - CompletionCandidate candidate = new CompletionCandidate(text, description); + CompletionCandidate candidate = new CompletionCandidate(Arrays.asList(text.split(" ")), + description, complete); return candidate; } From bf984c1197acb9d54e5e450f21c592fcaa261fe6 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:05:51 +0200 Subject: [PATCH 07/14] Pass completion status to candidate --- .../ide/completion/CompletionCandidateCollectorDefault.java | 3 ++- .../java/com/devonfw/tools/ide/completion/IdeCompleter.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java index 33f6fc25fb..91da2eb82d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java @@ -48,7 +48,8 @@ public void add(String text, String description, Property property, Commandle } } - CompletionCandidate candidate = createCandidate(text, description, property, commandlet); + CompletionCandidate candidate = this.createCandidate(text, description, property, commandlet, + !text.endsWith("=")); this.candidates.add(candidate); LOG.trace("Added {} for auto-completion of property {}.{}", candidate, commandlet, property); } 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 3051cf309c..7e211283ac 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 @@ -35,7 +35,8 @@ public void complete(LineReader reader, ParsedLine commandLine, List 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++)); + candidates.add(new Candidate(candidate.text(), candidate.text(), null, null, null, null, + candidate.complete(), i++)); } } From 0830786562713057f5e3822606c47a436b02c9ee Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:07:15 +0200 Subject: [PATCH 08/14] Fix commandlet-specific flag completion --- .../tools/ide/context/AbstractIdeContext.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 f810c7d4d1..037d7caa5b 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 @@ -1442,7 +1442,10 @@ public List complete(CliArguments arguments, boolean includ property.apply(arguments, this, cc, collector); } } - Iterator commandletIterator = this.commandletManager.findCommandlet(arguments, collector); + + this.commandletManager.collectCompletionCandidates(arguments, collector); + + Iterator commandletIterator = this.commandletManager.findCommandlet(arguments, null); CliArgument current = arguments.current(); if (current.isCompletion() && current.isCombinedShortOption()) { collector.add(current.get(), null, null, null); @@ -1461,6 +1464,7 @@ private void completeCommandlet(CliArguments arguments, Commandlet cmd, Completi LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName()); Iterator> valueIterator = cmd.getValues().iterator(); + Property lastValueProperty = null; 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 @@ -1499,18 +1503,30 @@ private void completeCommandlet(CliArguments arguments, Commandlet cmd, Completi } } } else { + if (currentArgument.isCompletion() && currentArgument.get().isEmpty() + && !arguments.isEndOptions()) { + for (Property option : optionProperties) { + option.apply(arguments, this, cmd, collector); + } + } + + Property valueProperty = null; if (valueIterator.hasNext()) { - Property valueProperty = valueIterator.next(); + valueProperty = valueIterator.next(); boolean success = valueProperty.apply(arguments, this, cmd, collector); if (!success) { LOG.trace("Completion cannot match any further."); return; } + } else if (lastValueProperty != null && lastValueProperty.isMultiValued()) { + valueProperty = lastValueProperty; } else { LOG.trace("No value left for completion."); return; } } + + arguments.next(); currentArgument = arguments.current(); } } From 6e66a969eff1d5c3c1c97057c98286a82bf80d58 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:09:04 +0200 Subject: [PATCH 09/14] Make completeValue() abstract --- .../com/devonfw/tools/ide/property/BooleanProperty.java | 4 ++++ .../com/devonfw/tools/ide/property/EditionProperty.java | 6 ++++++ .../java/com/devonfw/tools/ide/property/NumberProperty.java | 5 +++++ .../main/java/com/devonfw/tools/ide/property/Property.java | 4 +--- .../java/com/devonfw/tools/ide/property/StringProperty.java | 6 ++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/BooleanProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/BooleanProperty.java index 680b62b1f3..51069a604d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/BooleanProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/BooleanProperty.java @@ -54,6 +54,10 @@ public Boolean parse(String valueAsString, IdeContext context) { return result; } + @Override + protected void completeValue(String arg, IdeContext contextual, Commandlet commandlet, + CompletionCandidateCollector collector) {} + private Boolean parse(String valueAsString) { if (valueAsString == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java index 7cc2d782b2..564d034232 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.property; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.validation.PropertyValidator; @@ -41,4 +43,8 @@ public String parse(String valueAsString, IdeContext context) { return valueAsString; } + + @Override + protected void completeValue(String arg, IdeContext contextual, Commandlet commandlet, + CompletionCandidateCollector collector) {} } diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/NumberProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/NumberProperty.java index f13c59bb32..e6a433ef2b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/NumberProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/NumberProperty.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.property; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.validation.PropertyValidator; @@ -49,4 +51,7 @@ public Long parse(String valueAsString, IdeContext context) { } } + @Override + protected void completeValue(String arg, IdeContext contextual, Commandlet commandlet, + CompletionCandidateCollector collector) {} } 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 dbda7b1271..d27eaf2b4d 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 @@ -472,9 +472,7 @@ protected void complete(String normalizedName, CliArgument argument, CliArgument * @param commandlet the {@link Commandlet} owning this {@link Property}. * @param collector the {@link CompletionCandidateCollector}. */ - protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - - } + protected abstract void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector); /** * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match. diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/StringProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/StringProperty.java index b9ff9c4796..b5f0ecdb79 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/StringProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/StringProperty.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.property; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.validation.PropertyValidator; @@ -59,6 +61,10 @@ public String parse(String valueAsString, IdeContext context) { return valueAsString; } + @Override + protected void completeValue(String arg, IdeContext contextual, Commandlet commandlet, + CompletionCandidateCollector collector) {} + /** * @return the {@link #getValue() value} as null-safe {@link String} array. */ From 7d9135a1ae8347697a87ea1d943c61e1d354db48 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:10:49 +0200 Subject: [PATCH 10/14] Add specific multi-value maven goal property for autocompletion --- .../tools/ide/property/MvnArgProperty.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java new file mode 100644 index 0000000000..1a44dac8f3 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java @@ -0,0 +1,50 @@ +package com.devonfw.tools.ide.property; + +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidateCollector; +import com.devonfw.tools.ide.context.IdeContext; +import java.util.List; + +public class MvnArgProperty extends StringProperty { + private static final List MAVEN_GOALS = List.of( + "clean", + "compile", + "dependency:list", + "dependency:tree", + "deploy", + "exec:java", + "generate-resources", + "generate-sources", + "help:effective-settings", + "install", + "integration-test", + "package", + "post-clean", + "post-integration-test", + "prepare-package", + "pre-clean", + "pre-integration-test", + "process-resources", + "process-sources", + "site", + "site-deploy", + "test", + "test-compile", + "validate", + "verify" + ); + + public MvnArgProperty(String name, String alias) { + super(name, true, alias); + } + + @Override + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, + CompletionCandidateCollector collector) { + for (String goal : MAVEN_GOALS) { + if (goal.startsWith(arg)) { + collector.add(goal, null, this, commandlet); + } + } + } +} From 6408e057d1576902d0f9358849b3416c348cb47c Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:11:17 +0200 Subject: [PATCH 11/14] Adjust createCandidate() call --- .../java/com/devonfw/tools/ide/property/VersionProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java index 759512adc9..b70c85418a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java @@ -88,7 +88,7 @@ private void completeVersion(VersionIdentifier version2complete, ToolCommandlet List candidates = collector.getCandidates(); Collections.reverse(candidates); CompletionCandidate latest = collector.createCandidate(text + VersionSegment.PATTERN_MATCH_ANY_STABLE_VERSION, - "Latest stable matching version", this, commandlet); + "Latest stable matching version", this, commandlet, true); if (candidates.isEmpty()) { candidates.add(latest); } else { From d827eccd710bae1ad109bf75d0ac947ce1d46441 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:13:02 +0200 Subject: [PATCH 12/14] Add properties to Mvn commandlet --- .../com/devonfw/tools/ide/tool/mvn/Mvn.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java index cf26227fa4..a2c3b967b5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java @@ -20,6 +20,8 @@ import com.devonfw.tools.ide.process.ProcessContext; import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.process.ProcessResult; +import com.devonfw.tools.ide.property.FlagProperty; +import com.devonfw.tools.ide.property.MvnArgProperty; import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.ToolCommandlet; @@ -62,6 +64,20 @@ public class Mvn extends LocalToolCommandlet { private static final VariableSyntax VARIABLE_SYNTAX = VariableSyntax.SQUARE; + private final FlagProperty alsoMake; + private final FlagProperty alsoMakeDependents; + private final FlagProperty failAtEnd; + private final FlagProperty failFast; + private final FlagProperty threads; + + private final FlagProperty skipTests; + private final FlagProperty deployAtEnd; + + private final FlagProperty execMainClass; + private final FlagProperty execArgs; + + private final MvnArgProperty goals; + /** * The constructor. * @@ -70,6 +86,24 @@ public class Mvn extends LocalToolCommandlet { public Mvn(IdeContext context) { super(context, "mvn", Set.of(Tag.JAVA, Tag.BUILD)); + + this.alsoMake = this.add(new FlagProperty("--also-make", false, "-am")); + this.alsoMakeDependents = this.add(new FlagProperty("--also-make-dependents", false, "-amd")); + this.failAtEnd = this.add(new FlagProperty("--fail-at-end", false, "-fae")); + this.failFast = this.add(new FlagProperty("--fail-fast", false, "-ff")); + this.threads = this.add(new FlagProperty("--threads", false, "-t")); + + this.skipTests = this.add(new FlagProperty("--define skipTests", false, "-DskipTests")); + this.deployAtEnd = this.add(new FlagProperty("--define deployAtEnd", false, "-DdeployAtEnd")); + + this.execMainClass = this.add( + new FlagProperty("--define exec.mainClass=", false, "-Dexec.mainClass=") + ); + this.execArgs = this.add( + new FlagProperty("--define exec.args=", false, "-Dexec.args=") + ); + + this.goals = this.add(new MvnArgProperty("goals", "")); } @Override From 365c64850bb64bd2bad46f8858e7ef5f62b393c2 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Mon, 27 Apr 2026 11:13:37 +0200 Subject: [PATCH 13/14] Comment out potentially wrong check --- .../java/com/devonfw/tools/ide/commandlet/Commandlet.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 76dd91ebe8..135eef2f09 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 @@ -121,9 +121,9 @@ protected void addKeyword(String keyword, String alias) { */ protected

> P add(P property) { - if (this.multiValued != null) { - throw new IllegalStateException("The multi-valued property " + this.multiValued + " can not be followed by " + property); - } + // if (this.multiValued != null) { + // throw new IllegalStateException("The multi-valued property " + this.multiValued + " can not be followed by " + property); + // } this.propertiesList.add(property); if (property.isOption()) { add(property.getName(), property, false); From 499ecbec557ebd0b53e7addffd453d77bbf485b1 Mon Sep 17 00:00:00 2001 From: Henok Lachmann Date: Tue, 28 Apr 2026 08:33:35 +0200 Subject: [PATCH 14/14] Fix bug to allow Mvn their own properties --- .../java/com/devonfw/tools/ide/commandlet/Commandlet.java | 6 +++--- .../com/devonfw/tools/ide/context/AbstractIdeContext.java | 1 - .../java/com/devonfw/tools/ide/property/MvnArgProperty.java | 2 +- .../java/com/devonfw/tools/ide/tool/ToolCommandlet.java | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) 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 135eef2f09..76dd91ebe8 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 @@ -121,9 +121,9 @@ protected void addKeyword(String keyword, String alias) { */ protected

> P add(P property) { - // if (this.multiValued != null) { - // throw new IllegalStateException("The multi-valued property " + this.multiValued + " can not be followed by " + property); - // } + if (this.multiValued != null) { + throw new IllegalStateException("The multi-valued property " + this.multiValued + " can not be followed by " + property); + } this.propertiesList.add(property); if (property.isOption()) { add(property.getName(), property, false); 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 037d7caa5b..c793dbf8cd 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 @@ -1465,7 +1465,6 @@ private void completeCommandlet(CliArguments arguments, Commandlet cmd, Completi LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName()); Iterator> valueIterator = cmd.getValues().iterator(); Property lastValueProperty = null; - 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()); diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java index 1a44dac8f3..5d748539ab 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/MvnArgProperty.java @@ -35,7 +35,7 @@ public class MvnArgProperty extends StringProperty { ); public MvnArgProperty(String name, String alias) { - super(name, true, alias); + super(name, true, true, alias); } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java index 2097c3453a..a5cd9c5fc6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java @@ -73,7 +73,6 @@ public ToolCommandlet(IdeContext context, String tool, Set tags) { this.tags = tags; addKeyword(tool); this.arguments = new StringProperty("", false, true, "args"); - initProperties(); } /**