diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 74e0e5bb3b..e7e497a6d5 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2193[#2193]: Added support for conditional auto-completion * https://github.com/devonfw/IDEasy/issues/1525[#1525]: Document known issue and workaround for lombok plugin in Eclipse * https://github.com/devonfw/IDEasy/issues/1031[#1031]: Added OpenRewrite commandlet * https://github.com/devonfw/IDEasy/issues/2361[#2361]: Improve dotnet installation by setting DOTNET_ROOT diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/AutoCompletionRegistry.java b/cli/src/main/java/com/devonfw/tools/ide/completion/AutoCompletionRegistry.java index 7948f360a1..368d8de349 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/AutoCompletionRegistry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/AutoCompletionRegistry.java @@ -39,7 +39,6 @@ public void add(String candidate, String synonym) { this.entries.add(entry); } - /** * Adds all candidates matching the given argument to the collector. * @@ -56,5 +55,61 @@ public void complete(String arg, CompletionCandidateCollector collector, } } + /** + * Registers two already-added candidates as alternatives to each other, so that once one is provided on the command line, + * the other is no longer be suggested. + * + * @param candidate1 the text of the first candidate (must have been added via {@link #add(String)} before). + * @param candidate2 the text of the second candidate (must have been added via {@link #add(String)} before). + * @throws IllegalStateException if either candidate has not been registered via {@link #add(String)}. + */ + public void addAlternative(String candidate1, String candidate2) { + + CompletionEntry entry1 = findEntry(candidate1); + CompletionEntry entry2 = findEntry(candidate2); + if ((entry1 == null) || (entry2 == null)) { + throw new IllegalStateException("Both candidates must be added via add(String) before calling addAlternative."); + } + entry1.addAlternative(entry2); + } + + /** + * Registers a dependency for {@code candidate}: it is only suggested once at least one of {@code depends} has already been provided on the command line. + * + * @param candidate the text of the dependent candidate (must have been added via {@link #add(String)} before). + * @param depends the texts of the candidates of which at least one must already be provided (OR semantics). + * @throws IllegalStateException if {@code candidate} or any of {@code depends} has not been registered via {@link #add(String)}. + */ + public void addDependency(String candidate, List depends) { + CompletionEntry entry = findEntry(candidate); + if (entry == null) { + throw new IllegalStateException("Candidate '" + candidate + "' must be added via add(String) before calling addDependency."); + } + + CompletionEntry[] dependencyEntries = new CompletionEntry[depends.size()]; + for (int i = 0; i < depends.size(); i++) { + CompletionEntry dependencyEntry = findEntry(depends.get(i)); + + if (dependencyEntry == null) { + throw new IllegalStateException("Candidate '" + depends.get(i) + "' must be added via add(String) before calling addDependency."); + } + + dependencyEntries[i] = dependencyEntry; + } + entry.addDependency(dependencyEntries); + } + + /** + * @param candidate the candidate to find. + * @return the {@link CompletionEntry} whose {@link CompletionEntry#getCandidate() candidate} matches, or {@code null} if not found. + */ + private CompletionEntry findEntry(String candidate) { + for (CompletionEntry entry : this.entries) { + if (entry.getCandidate().equals(candidate)) { + return entry; + } + } + return null; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionEntry.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionEntry.java index 9df9cc7439..da5c38ee78 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionEntry.java @@ -8,8 +8,8 @@ import com.devonfw.tools.ide.property.Property; /** - * A completion candidate that may have one or more synonyms. When any one of the candidate or its synonyms has already been provided on the command line, none - * of them will be suggested again. + * A completion candidate that may have synonyms, alternatives, or dependencies. An entry will not be suggested if it or its synonyms/alternatives are already + * provided, or if its dependencies are not satisfied. */ public class CompletionEntry { @@ -19,6 +19,12 @@ public class CompletionEntry { /** List of synonym strings for this candidate. */ private List synonyms = new ArrayList<>(); + /** List of alternatives (symmetric relationship). */ + private List alternatives = new ArrayList<>(); + + /** List of dependency groups (AND logic between groups, OR logic within a group). */ + private List> dependencies = new ArrayList<>(); + /** * The constructor. * @@ -28,6 +34,13 @@ public CompletionEntry(String candidate) { this.candidate = candidate; } + /** + * @return the primary candidate string. + */ + public String getCandidate() { + return candidate; + } + /** * Adds a synonym for this candidate. * @@ -48,8 +61,20 @@ public void addSynonym(String synonym) { public void complete(String arg, CompletionCandidateCollector collector, Property property, Commandlet commandlet) { Set alreadyProvided = collector.getAlreadyProvided(); - if (alreadyProvided != null && (alreadyProvided.contains(this.candidate) || synonyms.stream().anyMatch(alreadyProvided::contains))) { - return; + if (alreadyProvided != null) { + if (!isDependencySatisfied(alreadyProvided)) { + return; + } + + if (isProvided(alreadyProvided)) { + return; + } + + for (CompletionEntry alternative : this.alternatives) { + if (alternative.isProvided(alreadyProvided)) { + return; + } + } } if (candidate.startsWith(arg)) { @@ -63,4 +88,68 @@ public void complete(String arg, CompletionCandidateCollector collector, Propert } } + /** + * Checks whether all configured dependency groups are satisfied. + * + * @param alreadyProvided the set of already provided arguments. + * @return {@code true} if all dependency groups are satisfied, {@code false} otherwise. + */ + private boolean isDependencySatisfied(Set alreadyProvided) { + + for (List group : this.dependencies) { + boolean groupSatisfied = false; + for (CompletionEntry entry : group) { + if (entry.isProvided(alreadyProvided)) { + groupSatisfied = true; + break; + } + } + if (!groupSatisfied) { + return false; + } + } + return true; + } + + /** + * Checks if this candidate or any of its synonyms was already provided. + * + * @param alreadyProvided the set of already provided arguments. + * @return {@code true} if already provided, {@code false} otherwise. + */ + public boolean isProvided(Set alreadyProvided) { + return alreadyProvided.contains(this.candidate) || this.synonyms.stream().anyMatch(alreadyProvided::contains); + } + + /** + * Adds a symmetric alternative relation between this entry and another. + * + * @param alternative the alternative {@link CompletionEntry}. + */ + public void addAlternative(CompletionEntry alternative) { + + if ((alternative == null) || (alternative == this)) { + return; + } + + if (!this.alternatives.contains(alternative)) { + this.alternatives.add(alternative); + } + + if (!alternative.alternatives.contains(this)) { + alternative.alternatives.add(this); + } + } + + /** + * Adds an OR-dependency group to this entry. + * + * @param entries array of {@link CompletionEntry} objects of which at least one must be provided. + */ + public void addDependency(CompletionEntry[] entries) { + if ((entries == null) || (entries.length == 0)) { + return; + } + this.dependencies.add(List.of(entries)); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MavenCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MavenCommandlet.java index d843e41e1a..5d8184db16 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MavenCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MavenCommandlet.java @@ -1,9 +1,9 @@ package com.devonfw.tools.ide.tool.mvn; +import java.util.List; import java.util.Set; - import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.completion.AutoCompletionRegistry; import com.devonfw.tools.ide.context.IdeContext; @@ -48,9 +48,6 @@ protected void initAutoCompletionRegistry(AutoCompletionRegistry registry) { registry.add("help:effective-settings"); registry.add("-DskipTests"); registry.add("-Dmaven.test.skip=true"); - registry.add("exec:java"); - registry.add("-Dexec.mainClass="); - registry.add("-Dexec.args="); registry.add("-P"); registry.add("-pl"); registry.add("-am"); @@ -75,7 +72,12 @@ protected void initAutoCompletionRegistry(AutoCompletionRegistry registry) { registry.add("-Dstyle.color="); registry.add("-Duser.dir="); registry.add("-Duser.home="); + registry.add("exec:java"); + registry.add("exec:exec"); + registry.addAlternative("exec:java", "exec:exec"); + registry.add("-Dexec.mainClass="); + registry.addDependency("-Dexec.mainClass=", List.of("exec:java")); + registry.add("-Dexec.args="); + registry.addDependency("-Dexec.args=", List.of("exec:java", "exec:exec")); } } - - 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 eaa0052768..0d25e44ef9 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 @@ -407,8 +407,8 @@ void testSynonymFilteringWithProvidedSynonym() { } /** - * Test that completion works for a second tool argument (e.g. "ide mvn clean [tab]"), which is the real-world scenario - * that previously failed because the multivalued arguments property consumed the completion marker greedily. + * Test that completion works for a second tool argument (e.g. "ide mvn clean [tab]"), which is the real-world scenario that previously failed because the + * multivalued arguments property consumed the completion marker greedily. */ @Test void testCompleteMavenSecondToolArgument() { @@ -425,4 +425,101 @@ void testCompleteMavenSecondToolArgument() { assertThat(candidates.stream().map(CompletionCandidate::text)) .contains("dependency:list", "dependency:tree", "deploy"); } + + /** + * + */ + @Test + void testAlternativeFilteringWhenOtherAlternativeProvidedJava() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + String[] argsArray = { "mvn", "exec:java", "" }; + CliArguments args = CliArguments.ofCompletion(argsArray); + CompletionCandidateCollector collector = createCollector(context, argsArray); + + // act + List candidates = context.complete(args, collector, true); + + // assert + List texts = candidates.stream().map(CompletionCandidate::text).toList(); + assertThat(texts).doesNotContain("exec:exec"); + } + + /** + * + */ + @Test + void testAlternativeFilteringWhenOtherAlternativeProvidedExec() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + String[] argsArray = { "mvn", "exec:exec", "" }; + CliArguments args = CliArguments.ofCompletion(argsArray); + CompletionCandidateCollector collector = createCollector(context, argsArray); + + // act + List candidates = context.complete(args, collector, true); + + // assert + List texts = candidates.stream().map(CompletionCandidate::text).toList(); + assertThat(texts).doesNotContain("exec:java"); + } + + /** + * Test that an entry with an unsatisfied dependency is not suggested. + */ + @Test + void testDependencyNotSatisfiedIsNotSuggested() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + String[] argsArray = { "mvn", "-Dexec.main" }; + CliArguments args = CliArguments.ofCompletion(argsArray); + CompletionCandidateCollector collector = createCollector(context, argsArray); + + // act + List candidates = context.complete(args, collector, true); + + // assert + assertThat(candidates.stream().map(CompletionCandidate::text)).doesNotContain("-Dexec.mainClass="); + } + + /** + * Test that an entry with a satisfied dependency is suggested. + */ + @Test + void testDependencySatisfiedIsSuggested() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + String[] argsArray = { "mvn", "exec:java", "-Dexec.mainCla" }; + CliArguments args = CliArguments.ofCompletion(argsArray); + CompletionCandidateCollector collector = createCollector(context, argsArray); + + // act + List candidates = context.complete(args, collector, true); + + // assert + assertThat(candidates.stream().map(CompletionCandidate::text)).contains("-Dexec.mainClass="); + } + + /** + * Test that an entry with an OR-dependency is suggested if any alternative of the group is provided. + */ + @Test + void testDependencyOrGroupSatisfiedByEitherAlternative() { + + // arrange + AbstractIdeContext context = newContext(PROJECT_BASIC, null, false); + String[] argsArray = { "mvn", "exec:exec", "-Dexec.arg" }; + CliArguments args = CliArguments.ofCompletion(argsArray); + CompletionCandidateCollector collector = createCollector(context, argsArray); + + // act + List candidates = context.complete(args, collector, true); + + // assert + assertThat(candidates.stream().map(CompletionCandidate::text)).contains("-Dexec.args="); + } }