Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -19,6 +19,12 @@ public class CompletionEntry {
/** List of synonym strings for this candidate. */
private List<String> synonyms = new ArrayList<>();

/** List of alternatives (symmetric relationship). */
private List<CompletionEntry> alternatives = new ArrayList<>();

/** List of dependency groups (AND logic between groups, OR logic within a group). */
private List<List<CompletionEntry>> dependencies = new ArrayList<>();

/**
* The constructor.
*
Expand All @@ -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.
*
Expand All @@ -48,8 +61,20 @@ public void addSynonym(String synonym) {
public void complete(String arg, CompletionCandidateCollector collector, Property<?> property, Commandlet commandlet) {

Set<String> 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)) {
Expand All @@ -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<String> alreadyProvided) {

for (List<CompletionEntry> 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<String> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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"));
}
}


101 changes: 99 additions & 2 deletions cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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<CompletionCandidate> candidates = context.complete(args, collector, true);

// assert
List<String> 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<CompletionCandidate> candidates = context.complete(args, collector, true);

// assert
List<String> 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<CompletionCandidate> 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<CompletionCandidate> 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<CompletionCandidate> candidates = context.complete(args, collector, true);

// assert
assertThat(candidates.stream().map(CompletionCandidate::text)).contains("-Dexec.args=");
}
}