Skip to content

Commit d86a398

Browse files
committed
Improve LineReader completer updates, CTRL+C handling, and add tests
GrailsConsole improvements: - Use updateCompleter() with LineReaderImpl.setCompleter() instead of rebuilding the entire LineReader when completers are added - Add dedicated initializeHistory() method that properly attaches DefaultHistory to the LineReader after construction - CandidateListCompletionHandler: Auto-complete common prefix in buffer GrailsCli improvements: - Replace manual terminal attribute manipulation and input polling with JLine 3's native Terminal.Signal.INT handler for CTRL+C cancellation - This is the idiomatic way to handle interrupts in JLine 3 Test coverage: - Add comprehensive tests for GrailsConsole completer management - Add tests for CandidateListCompletionHandler - Add tests for all CLI completers: StringsCompleter, RegexCompletor, ClosureCompleter, SortedAggregateCompleter, EscapingFileNameCompletor, SimpleOrFileNameCompletor, and CommandCompleter
1 parent b4e78a0 commit d86a398

12 files changed

Lines changed: 2344 additions & 29 deletions

File tree

grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.jline.reader.History;
4040
import org.jline.reader.LineReader;
4141
import org.jline.reader.LineReaderBuilder;
42+
import org.jline.reader.impl.LineReaderImpl;
4243
import org.jline.reader.impl.completer.AggregateCompleter;
4344
import org.jline.reader.impl.history.DefaultHistory;
4445
import org.jline.terminal.Terminal;
@@ -193,11 +194,27 @@ protected void initialize(InputStream systemIn, PrintStream systemOut, PrintStre
193194
terminal = createTerminal();
194195
history = prepareHistory();
195196
reader = createLineReader(terminal, history);
197+
initializeHistory();
196198
} else if (isActivateTerminal()) {
197199
terminal = createTerminal();
198200
}
199201
}
200202

203+
/**
204+
* Initializes history by attaching it to the reader and loading existing entries.
205+
* This must be called after the LineReader is fully constructed.
206+
*/
207+
private void initializeHistory() {
208+
if (history instanceof DefaultHistory && reader != null) {
209+
DefaultHistory defaultHistory = (DefaultHistory) history;
210+
try {
211+
defaultHistory.attach(reader);
212+
} catch (Exception e) {
213+
// History initialization failed, continue without persistent history
214+
}
215+
}
216+
}
217+
201218
protected void bindSystemOutAndErr(PrintStream systemOut, PrintStream systemErr) {
202219
originalSystemOut = unwrapPrintStream(systemOut);
203220
out = originalSystemOut;
@@ -263,39 +280,31 @@ protected Terminal createTerminal() throws IOException {
263280

264281
public void resetCompleters() {
265282
completers.clear();
266-
rebuildLineReader();
283+
updateCompleter();
267284
}
268285

269286
public void addCompleter(Completer completer) {
270287
if (completer != null) {
271288
completers.add(completer);
272-
rebuildLineReader();
289+
updateCompleter();
273290
}
274291
}
275292

276293
/**
277-
* Rebuilds the LineReader with all registered completers using an AggregateCompleter.
278-
* This is necessary because JLine 3 LineReader is immutable once created.
294+
* Updates the LineReader completer using an AggregateCompleter when needed.
279295
*/
280-
private void rebuildLineReader() {
281-
if (terminal != null) {
282-
try {
283-
LineReaderBuilder builder = LineReaderBuilder.builder()
284-
.terminal(terminal)
285-
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true);
286-
287-
if (!completers.isEmpty()) {
288-
builder.completer(new AggregateCompleter(completers));
289-
}
290-
291-
if (history != null) {
292-
builder.variable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath());
293-
builder.history(history);
294-
}
295-
reader = builder.build();
296-
} catch (Exception e) {
297-
// ignore
298-
}
296+
private void updateCompleter() {
297+
if (reader == null) {
298+
return;
299+
}
300+
if (!(reader instanceof LineReaderImpl)) {
301+
return;
302+
}
303+
LineReaderImpl lineReader = (LineReaderImpl) reader;
304+
if (completers.isEmpty()) {
305+
lineReader.setCompleter(null);
306+
} else {
307+
lineReader.setCompleter(new AggregateCompleter(completers));
299308
}
300309
}
301310

grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ public void complete(LineReader reader, ParsedLine line, List<Candidate> candida
5050
if (delegate != null) {
5151
delegate.complete(reader, line, candidates);
5252
}
53+
54+
if (reader == null) {
55+
return;
56+
}
57+
58+
String commonPrefix = getUnambiguousCompletions(candidates);
59+
if (commonPrefix == null) {
60+
return;
61+
}
62+
63+
String current = line != null ? line.word() : "";
64+
if (current == null) {
65+
current = "";
66+
}
67+
68+
if (commonPrefix.startsWith(current) && !commonPrefix.equals(current)) {
69+
String suffix = commonPrefix.substring(current.length());
70+
reader.getBuffer().write(suffix);
71+
reader.callWidget(LineReader.REDRAW_LINE);
72+
}
5373
}
5474

5575
/**

0 commit comments

Comments
 (0)