Skip to content

Commit b6d2ecc

Browse files
authored
#189: completion shell using jline (#178)
1 parent e180912 commit b6d2ecc

23 files changed

+972
-81
lines changed

cli/pom.xml

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<releaseName>${project.artifactId}-${os.detected.classifier}-${os.detected.arch}</releaseName>
1818
<java.version>17</java.version>
1919
<native.maven.plugin.version>0.9.28</native.maven.plugin.version>
20+
<jline.version>3.24.1</jline.version>
21+
<jansi.version>2.4.0</jansi.version>
2022
</properties>
2123

2224
<dependencies>
@@ -75,6 +77,16 @@
7577
<artifactId>progressbar</artifactId>
7678
<version>0.10.0</version>
7779
</dependency>
80+
<dependency>
81+
<groupId>org.jline</groupId>
82+
<artifactId>jline</artifactId>
83+
<version>${jline.version}</version>
84+
</dependency>
85+
<dependency>
86+
<groupId>org.fusesource.jansi</groupId>
87+
<artifactId>jansi</artifactId>
88+
<version>${jansi.version}</version>
89+
</dependency>
7890
</dependencies>
7991

8092
<build>

cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.devonfw.tools.ide.context.IdeContext;
1212
import com.devonfw.tools.ide.property.KeywordProperty;
1313
import com.devonfw.tools.ide.property.Property;
14+
import com.devonfw.tools.ide.tool.ToolCommandlet;
1415
import com.devonfw.tools.ide.version.VersionIdentifier;
1516

1617
/**
@@ -208,12 +209,11 @@ public String toString() {
208209
}
209210

210211
/**
211-
* @param version the {@link VersionIdentifier} to complete.
212-
* @param collector the {@link CompletionCandidateCollector}.
213-
* @return {@code true} on success, {@code false} otherwise.
212+
* @return the {@link ToolCommandlet} set in a {@link Property} of this commandlet used for auto-completion of a
213+
* {@link VersionIdentifier} or {@code null} if not exists or not configured.
214214
*/
215-
public boolean completeVersion(VersionIdentifier version, CompletionCandidateCollector collector) {
215+
public ToolCommandlet getToolForVersionCompletion() {
216216

217-
return false;
217+
return null;
218218
}
219219
}

cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public CommandletManagerImpl(IdeContext context) {
5252
add(new HelpCommandlet(context));
5353
add(new EnvironmentCommandlet(context));
5454
add(new CompleteCommandlet(context));
55+
add(new ShellCommandlet(context));
5556
add(new InstallCommandlet(context));
5657
add(new VersionSetCommandlet(context));
5758
add(new VersionGetCommandlet(context));

cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java

+5
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ public void run() {
4949
commandlet.install(false);
5050
}
5151

52+
@Override
53+
public ToolCommandlet getToolForVersionCompletion() {
54+
55+
return this.tool.getValue();
56+
}
5257
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package com.devonfw.tools.ide.commandlet;
2+
3+
import java.io.IOException;
4+
import java.util.Iterator;
5+
6+
import org.fusesource.jansi.AnsiConsole;
7+
import org.jline.reader.EndOfFileException;
8+
import org.jline.reader.LineReader;
9+
import org.jline.reader.LineReaderBuilder;
10+
import org.jline.reader.MaskingCallback;
11+
import org.jline.reader.Parser;
12+
import org.jline.reader.UserInterruptException;
13+
import org.jline.reader.impl.DefaultParser;
14+
import org.jline.terminal.Terminal;
15+
import org.jline.terminal.TerminalBuilder;
16+
import org.jline.widget.AutosuggestionWidgets;
17+
18+
import com.devonfw.tools.ide.cli.CliArgument;
19+
import com.devonfw.tools.ide.cli.CliArguments;
20+
import com.devonfw.tools.ide.cli.CliException;
21+
import com.devonfw.tools.ide.completion.IdeCompleter;
22+
import com.devonfw.tools.ide.context.AbstractIdeContext;
23+
import com.devonfw.tools.ide.context.IdeContext;
24+
import com.devonfw.tools.ide.property.BooleanProperty;
25+
import com.devonfw.tools.ide.property.FlagProperty;
26+
import com.devonfw.tools.ide.property.KeywordProperty;
27+
import com.devonfw.tools.ide.property.Property;
28+
29+
/**
30+
* {@link Commandlet} for internal interactive shell with build-in auto-completion and help.
31+
*/
32+
public final class ShellCommandlet extends Commandlet {
33+
34+
private static final int AUTOCOMPLETER_MAX_RESULTS = 50;
35+
36+
private static final int RC_EXIT = 987654321;
37+
38+
/**
39+
* The constructor.
40+
*
41+
* @param context the {@link IdeContext}.
42+
*/
43+
public ShellCommandlet(IdeContext context) {
44+
45+
super(context);
46+
addKeyword(getName());
47+
}
48+
49+
@Override
50+
public String getName() {
51+
52+
return "shell";
53+
}
54+
55+
@Override
56+
public boolean isIdeHomeRequired() {
57+
58+
return false;
59+
}
60+
61+
@Override
62+
public void run() {
63+
64+
try {
65+
// TODO: add BuiltIns here, see: https://github.com/devonfw/IDEasy/issues/168
66+
67+
Parser parser = new DefaultParser();
68+
try (Terminal terminal = TerminalBuilder.builder().build()) {
69+
70+
// initialize our own completer here
71+
IdeCompleter completer = new IdeCompleter((AbstractIdeContext) this.context);
72+
73+
LineReader reader = LineReaderBuilder.builder().terminal(terminal).completer(completer).parser(parser)
74+
.variable(LineReader.LIST_MAX, AUTOCOMPLETER_MAX_RESULTS).build();
75+
76+
// Create autosuggestion widgets
77+
AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader);
78+
// Enable autosuggestions
79+
autosuggestionWidgets.enable();
80+
81+
// TODO: implement TailTipWidgets, see: https://github.com/devonfw/IDEasy/issues/169
82+
83+
String prompt = "ide> ";
84+
String rightPrompt = null;
85+
String line;
86+
87+
AnsiConsole.systemInstall();
88+
while (true) {
89+
try {
90+
line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
91+
reader.getHistory().add(line);
92+
int rc = runCommand(line);
93+
if (rc == RC_EXIT) {
94+
return;
95+
}
96+
} catch (UserInterruptException e) {
97+
// Ignore CTRL+C
98+
return;
99+
} catch (EndOfFileException e) {
100+
// CTRL+D
101+
return;
102+
} finally {
103+
AnsiConsole.systemUninstall();
104+
}
105+
}
106+
107+
} catch (IOException e) {
108+
throw new RuntimeException(e);
109+
}
110+
} catch (Exception e) {
111+
throw new RuntimeException("Unexpected error during interactive auto-completion", e);
112+
}
113+
}
114+
115+
/**
116+
* Converts String of arguments to array and runs the command
117+
*
118+
* @param args String of arguments
119+
* @return status code
120+
*/
121+
private int runCommand(String args) {
122+
123+
if ("exit".equals(args) || "quit".equals(args)) {
124+
return RC_EXIT;
125+
}
126+
String[] arguments = args.split(" ", 0);
127+
CliArguments cliArgs = new CliArguments(arguments);
128+
cliArgs.next();
129+
return ((AbstractIdeContext) this.context).run(cliArgs);
130+
}
131+
132+
/**
133+
* @param argument the current {@link CliArgument} (position) to match.
134+
* @param commandlet the potential {@link Commandlet} to match.
135+
* @return {@code true} if the given {@link Commandlet} matches to the given {@link CliArgument}(s) and those have
136+
* been applied (set in the {@link Commandlet} and {@link Commandlet#validate() validated}), {@code false}
137+
* otherwise (the {@link Commandlet} did not match and we have to try a different candidate).
138+
*/
139+
private boolean apply(CliArgument argument, Commandlet commandlet) {
140+
141+
this.context.trace("Trying to match arguments to commandlet {}", commandlet.getName());
142+
CliArgument currentArgument = argument;
143+
Iterator<Property<?>> valueIterator = commandlet.getValues().iterator();
144+
Property<?> currentProperty = null;
145+
boolean endOpts = false;
146+
while (!currentArgument.isEnd()) {
147+
if (currentArgument.isEndOptions()) {
148+
endOpts = true;
149+
} else {
150+
String arg = currentArgument.get();
151+
this.context.trace("Trying to match argument '{}'", currentArgument);
152+
if ((currentProperty != null) && (currentProperty.isExpectValue())) {
153+
currentProperty.setValueAsString(arg, this.context);
154+
if (!currentProperty.isMultiValued()) {
155+
currentProperty = null;
156+
}
157+
} else {
158+
Property<?> property = null;
159+
if (!endOpts) {
160+
property = commandlet.getOption(currentArgument.getKey());
161+
}
162+
if (property == null) {
163+
if (!valueIterator.hasNext()) {
164+
this.context.trace("No option or next value found");
165+
return false;
166+
}
167+
currentProperty = valueIterator.next();
168+
this.context.trace("Next value candidate is {}", currentProperty);
169+
if (currentProperty instanceof KeywordProperty) {
170+
KeywordProperty keyword = (KeywordProperty) currentProperty;
171+
if (keyword.matches(arg)) {
172+
keyword.setValue(Boolean.TRUE);
173+
this.context.trace("Keyword matched");
174+
} else {
175+
this.context.trace("Missing keyword");
176+
return false;
177+
}
178+
} else {
179+
boolean success = currentProperty.assignValueAsString(arg, this.context, commandlet);
180+
if (!success && currentProperty.isRequired()) {
181+
return false;
182+
}
183+
}
184+
if ((currentProperty != null) && !currentProperty.isMultiValued()) {
185+
currentProperty = null;
186+
}
187+
} else {
188+
this.context.trace("Found option by name");
189+
String value = currentArgument.getValue();
190+
if (value != null) {
191+
property.setValueAsString(value, this.context);
192+
} else if (property instanceof BooleanProperty) {
193+
((BooleanProperty) property).setValue(Boolean.TRUE);
194+
} else {
195+
currentProperty = property;
196+
if (property.isEndOptions()) {
197+
endOpts = true;
198+
}
199+
throw new UnsupportedOperationException("not implemented");
200+
}
201+
}
202+
}
203+
}
204+
currentArgument = currentArgument.getNext(!endOpts);
205+
}
206+
return commandlet.validate();
207+
}
208+
}

cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionSetCommandlet.java

+2-24
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,9 @@ public void run() {
5252
}
5353

5454
@Override
55-
public boolean completeVersion(VersionIdentifier version2complete, CompletionCandidateCollector collector) {
55+
public ToolCommandlet getToolForVersionCompletion() {
5656

57-
ToolCommandlet toolCmd = this.tool.getValue();
58-
if (toolCmd != null) {
59-
String text;
60-
if (version2complete == null) {
61-
text = "";
62-
} else {
63-
text = version2complete.toString();
64-
if (version2complete.isPattern()) {
65-
collector.add(text, this.version, this);
66-
return true;
67-
}
68-
}
69-
collector.add(text + VersionSegment.PATTERN_MATCH_ANY_STABLE_VERSION, this.tool, this);
70-
collector.add(text + VersionSegment.PATTERN_MATCH_ANY_VERSION, this.tool, this);
71-
List<VersionIdentifier> versions = this.context.getUrls().getSortedVersions(toolCmd.getName(),
72-
toolCmd.getEdition());
73-
int size = versions.size();
74-
String[] sorderCandidates = IntStream.rangeClosed(1, size).mapToObj(i -> versions.get(size - i).toString())
75-
.toArray(s -> new String[s]);
76-
collector.addAllMatches(text, sorderCandidates, this.version, this);
77-
return true;
78-
}
79-
return super.completeVersion(version2complete, collector);
57+
return this.tool.getValue();
8058
}
8159

8260
}

cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.devonfw.tools.ide.completion;
22

3+
import com.devonfw.tools.ide.version.VersionSegment;
4+
35
/**
46
* Candidate for auto-completion.
57
*
68
* @param text the text to suggest (CLI argument value).
9+
* @param description the description of the candidate.
710
*/
8-
public record CompletionCandidate(String text /* , String description */) implements Comparable<CompletionCandidate> {
11+
public record CompletionCandidate(String text, String description) implements Comparable<CompletionCandidate> {
912

1013
@Override
1114
public int compareTo(CompletionCandidate o) {

0 commit comments

Comments
 (0)