This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch issue-13752-upgrade-jansi-jline in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 958bfea6642a4ff0af1a1c5b3fc39c73b30a3921 Author: James Fredley <[email protected]> AuthorDate: Fri Jan 30 18:57:20 2026 -0500 Upgrade JLine to 3.27.1 and Jansi to 2.4.1 Closes #13752 - Update jline from 2.14.6 to 3.27.1 (org.jline:jline) - Update jansi from 1.18 to 2.4.1 - Migrate all CLI code from JLine 2 API to JLine 3 API: - ConsoleReader -> LineReader/Terminal - jline.console.completer.Completer -> org.jline.reader.Completer - complete(buffer, cursor, candidates) -> complete(reader, line, candidates) - CharSequence candidates -> Candidate objects - Keep JLine 2 (jline:jline:2.14.6) for groovy-groovysh compatibility (Groovy 4.x groovysh requires JLine 2; Groovy 5.x uses JLine 3) - Add TODO comments for JLine 2 removal when upgrading to Groovy 5 - Add JLine 3.27.1 license mapping in SbomPlugin (BSD-3-Clause) - Update RegexCompletorSpec tests for JLine 3 API --- .../org/apache/grails/buildsrc/SbomPlugin.groovy | 3 +- dependencies.gradle | 10 +- gradle/docs-dependencies.gradle | 1 + grails-bootstrap/build.gradle | 4 +- .../groovy/grails/build/logging/GrailsConsole.java | 156 ++++++++++----------- .../grails/build/logging/GrailsEclipseConsole.java | 12 +- .../CandidateListCompletionHandler.java | 70 ++++----- .../grails/build/logging/GrailsConsoleSpec.groovy | 18 ++- grails-console/build.gradle | 2 + grails-controllers/build.gradle | 2 +- grails-forge/gradle.properties | 2 +- grails-gradle/gradle/docs-config.gradle | 1 + grails-gradle/model/build.gradle | 3 +- .../grails/cli/profile/CommandDescription.groovy | 2 +- grails-scaffolding/build.gradle | 3 +- grails-shell-cli/build.gradle | 2 +- .../main/groovy/org/grails/cli/GrailsCli.groovy | 84 ++++++----- .../cli/gradle/commands/GradleCommand.groovy | 14 +- .../interactive/completers/ClosureCompleter.groovy | 11 +- .../completers/EscapingFileNameCompletor.groovy | 26 ++-- .../interactive/completers/RegexCompletor.groovy | 20 +-- .../completers/SimpleOrFileNameCompletor.groovy | 29 ++-- .../completers/SortedAggregateCompleter.java | 78 +++-------- .../interactive/completers/StringsCompleter.java | 37 ++--- .../org/grails/cli/profile/AbstractProfile.groovy | 4 +- .../groovy/org/grails/cli/profile/Profile.java | 4 +- .../commands/ArgumentCompletingCommand.groovy | 20 +-- .../cli/profile/commands/CommandCompleter.groovy | 11 +- .../cli/profile/commands/CreateAppCommand.groovy | 26 ++-- .../commands/DefaultMultiStepCommand.groovy | 2 +- .../grails/cli/profile/commands/HelpCommand.groovy | 13 +- .../grails/cli/profile/commands/OpenCommand.groovy | 11 +- .../test/groovy/org/grails/cli/TestTerminal.java | 29 +++- .../completers/RegexCompletorSpec.groovy | 26 ++-- grails-test-core/build.gradle | 2 +- grails-web-url-mappings/build.gradle | 2 +- 36 files changed, 381 insertions(+), 359 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index 6f3d7b9eea..01b7cdafa6 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -88,7 +88,8 @@ class SbomPlugin implements Plugin<Project> { private static Map<String, String> LICENSE_MAPPING = [ 'pkg:maven/org.antlr/[email protected]?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205 'pkg:maven/jline/[email protected]?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205 - 'pkg:maven/org.jline/[email protected]?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205 + 'pkg:maven/org.jline/[email protected]?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205 + 'pkg:maven/org.jline/[email protected]?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205 'pkg:maven/org.liquibase.ext/[email protected]?type=jar': 'Apache-2.0', // maps incorrectly because of https://github.com/liquibase/liquibase/issues/2445 & the base pom does not define a license 'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', // does not have map based on license id 'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', // does not have map based on license id diff --git a/dependencies.gradle b/dependencies.gradle index c87f4af529..c7ea63654a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -31,9 +31,11 @@ ext { 'directory-watcher.version' : '0.19.1', 'gradle-spock.version' : '2.3-groovy-3.0', 'grails-publish-plugin.version' : '0.0.4', - 'jansi.version' : '1.18', + 'jansi.version' : '2.4.1', 'javaparser-core.version' : '3.27.0', - 'jline.version' : '2.14.6', + 'jline.version' : '3.27.1', + // TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3) + 'jline2.version' : '2.14.6', 'jna.version' : '5.17.0', 'jquery.version' : '3.7.1', 'objenesis.version' : '3.4', @@ -59,7 +61,9 @@ ext { 'grails-publish-plugin' : "org.apache.grails.gradle:grails-publish:${gradleBomDependencyVersions['grails-publish-plugin.version']}", 'jansi' : "org.fusesource.jansi:jansi:${gradleBomDependencyVersions['jansi.version']}", 'javaparser-core' : "com.github.javaparser:javaparser-core:${gradleBomDependencyVersions['javaparser-core.version']}", - 'jline' : "jline:jline:${gradleBomDependencyVersions['jline.version']}", + 'jline' : "org.jline:jline:${gradleBomDependencyVersions['jline.version']}", + // TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3) + 'jline2' : "jline:jline:${gradleBomDependencyVersions['jline2.version']}", 'jna' : "net.java.dev.jna:jna:${gradleBomDependencyVersions['jna.version']}", 'objenesis' : "org.objenesis:objenesis:${gradleBomDependencyVersions['objenesis.version']}", 'spring-boot-cli' : "org.springframework.boot:spring-boot-cli:${gradleBomDependencyVersions['spring-boot.version']}", diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle index df4244f39c..828aaa7dc0 100644 --- a/gradle/docs-dependencies.gradle +++ b/gradle/docs-dependencies.gradle @@ -29,6 +29,7 @@ configurations.register('documentation') { dependencies { add('documentation', platform(project(':grails-bom'))) add('documentation', 'org.fusesource.jansi:jansi') + // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3) add('documentation', 'jline:jline') add('documentation', 'com.github.javaparser:javaparser-core') add('documentation', 'org.apache.groovy:groovy') diff --git a/grails-bootstrap/build.gradle b/grails-bootstrap/build.gradle index 22bf226e4f..934cda7785 100644 --- a/grails-bootstrap/build.gradle +++ b/grails-bootstrap/build.gradle @@ -50,7 +50,7 @@ dependencies { compileOnly 'io.methvin:directory-watcher' compileOnly 'org.fusesource.jansi:jansi' - compileOnly 'jline:jline' + compileOnly 'org.jline:jline' compileOnly 'net.java.dev.jna:jna' api 'org.yaml:snakeyaml' @@ -58,7 +58,7 @@ dependencies { testImplementation 'org.apache.groovy:groovy-xml' testImplementation 'org.apache.groovy:groovy-templates' testImplementation 'org.fusesource.jansi:jansi' - testImplementation 'jline:jline' + testImplementation 'org.jline:jline' testImplementation 'org.apache.groovy:groovy-test-junit5' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java index 986bf64083..dea79be276 100644 --- a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java +++ b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java @@ -18,14 +18,13 @@ */ package grails.build.logging; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.Flushable; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Stack; @@ -34,23 +33,21 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.StackTraceUtils; import org.codehaus.groovy.runtime.typehandling.NumberMath; -import jline.Terminal; -import jline.TerminalFactory; -import jline.UnixTerminal; -import jline.console.ConsoleReader; -import jline.console.completer.Completer; -import jline.console.history.FileHistory; -import jline.console.history.History; -import jline.internal.Log; -import jline.internal.ShutdownHooks; -import jline.internal.TerminalLineSettings; +import org.jline.reader.Completer; +import org.jline.reader.History; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.history.DefaultHistory; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; import org.apache.tools.ant.BuildException; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.Ansi.Color; import org.fusesource.jansi.AnsiConsole; import grails.util.Environment; -import org.grails.build.interactive.CandidateListCompletionHandler; import org.grails.build.logging.GrailsConsoleErrorPrintStream; import org.grails.build.logging.GrailsConsolePrintStream; @@ -114,7 +111,7 @@ public class GrailsConsole implements ConsoleLogger { /** * The reader to read info from the console */ - ConsoleReader reader; + LineReader reader; Terminal terminal; @@ -179,8 +176,8 @@ public class GrailsConsole implements ConsoleLogger { * @throws IOException */ public void reinitialize(InputStream systemIn, PrintStream systemOut, PrintStream systemErr) throws IOException { - if (reader != null) { - reader.shutdown(); + if (terminal != null) { + terminal.close(); } initialize(systemIn, systemOut, systemErr); } @@ -190,19 +187,12 @@ public class GrailsConsole implements ConsoleLogger { redirectSystemOutAndErr(true); - System.setProperty(ShutdownHooks.JLINE_SHUTDOWNHOOK, "false"); - if (isInteractiveEnabled()) { - reader = createConsoleReader(systemIn); - reader.setBellEnabled(false); - reader.setCompletionHandler(new CandidateListCompletionHandler()); - if (isActivateTerminal()) { - terminal = createTerminal(); - } - + terminal = createTerminal(); + reader = createLineReader(terminal); history = prepareHistory(); if (history != null) { - reader.setHistory(history); + reader.setVariable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath()); } } else if (isActivateTerminal()) { terminal = createTerminal(); @@ -251,51 +241,50 @@ public class GrailsConsole implements ConsoleLogger { return property == null ? true : Boolean.valueOf(property); } - protected ConsoleReader createConsoleReader(InputStream systemIn) throws IOException { - // need to swap out the output to avoid logging during init - final PrintStream nullOutput = new PrintStream(new ByteArrayOutputStream()); - final PrintStream originalOut = Log.getOutput(); - try { - Log.setOutput(nullOutput); - ConsoleReader consoleReader = new ConsoleReader(systemIn, out); - consoleReader.setExpandEvents(false); - return consoleReader; - } finally { - Log.setOutput(originalOut); - } + protected LineReader createLineReader(Terminal terminal) throws IOException { + LineReader lineReader = LineReaderBuilder.builder() + .terminal(terminal) + .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true) + .build(); + return lineReader; } /** - * Creates the instance of Terminal used directly in GrailsConsole. Note that there is also - * another terminal instance created implicitly inside of ConsoleReader. That instance - * is controlled by the jline.terminal system property. + * Creates the instance of Terminal used directly in GrailsConsole. */ - protected Terminal createTerminal() { - terminal = TerminalFactory.create(); - if (isWindows()) { - terminal.setEchoEnabled(true); - } + protected Terminal createTerminal() throws IOException { + Terminal terminal = TerminalBuilder.builder() + .system(true) + .build(); return terminal; } public void resetCompleters() { - final ConsoleReader reader = getReader(); - if (reader != null) { - Collection<Completer> completers = reader.getCompleters(); - for (Completer completer : completers) { - reader.removeCompleter(completer); - } + // In JLine 3, completers are set at LineReader creation time or via setCompleter + // We'll handle this differently - completers are managed via the LineReader + } - // for some unknown reason / bug in JLine you have to iterate over twice to clear the completers (WTF) - completers = reader.getCompleters(); - for (Completer completer : completers) { - reader.removeCompleter(completer); + public void addCompleter(Completer completer) { + // In JLine 3, we need to recreate the LineReader with the new completer + // or use an AggregateCompleter. For now, this is a simplified implementation. + if (terminal != null) { + try { + reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(completer) + .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true) + .build(); + if (history != null) { + reader.setVariable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath()); + } + } catch (Exception e) { + // ignore } } } /** - * Prepares a history file to be used by the ConsoleReader. This file + * Prepares a history file to be used by the LineReader. This file * will live in the home directory of the user. */ protected History prepareHistory() throws IOException { @@ -307,7 +296,7 @@ public class GrailsConsole implements ConsoleLogger { // can't create the file, so no history for you } } - return file.canWrite() ? new FileHistory(file) : null; + return file.canWrite() ? new DefaultHistory() : null; } public boolean isWindows() { @@ -334,8 +323,12 @@ public class GrailsConsole implements ConsoleLogger { if (instance != null) { instance.removeShutdownHook(); instance.restoreOriginalSystemOutAndErr(); - if (instance.getReader() != null) { - instance.getReader().shutdown(); + if (instance.terminal != null) { + try { + instance.terminal.close(); + } catch (IOException e) { + // ignore + } } instance = null; } @@ -348,24 +341,18 @@ public class GrailsConsole implements ConsoleLogger { protected void restoreTerminal() { try { - terminal.restore(); + if (terminal != null) { + terminal.close(); + } } catch (Exception e) { // ignore } - if (terminal instanceof UnixTerminal) { - // workaround for GRAILS-11494 - try { - new TerminalLineSettings().set("sane"); - } catch (Exception e) { - // ignore - } - } } protected void persistHistory() { - if (history instanceof Flushable) { + if (history != null && reader != null) { try { - ((Flushable) history).flush(); + history.save(); } catch (Throwable e) { // ignore exception } @@ -442,7 +429,7 @@ public class GrailsConsole implements ConsoleLogger { */ public InputStream getInput() { assertAllowInput(); - return reader.getInput(); + return terminal != null ? terminal.input() : System.in; } private void assertAllowInput() { @@ -471,7 +458,7 @@ public class GrailsConsole implements ConsoleLogger { this.lastMessage = lastMessage; } - public ConsoleReader getReader() { + public LineReader getReader() { return reader; } @@ -674,7 +661,7 @@ public class GrailsConsole implements ConsoleLogger { } public boolean isAnsiEnabled() { - return Ansi.isEnabled() && (terminal != null && terminal.isAnsiSupported()) && ansiEnabled; + return Ansi.isEnabled() && (terminal != null && terminal.getType() != Terminal.TYPE_DUMB) && ansiEnabled; } /** @@ -875,10 +862,15 @@ public class GrailsConsole implements ConsoleLogger { assertAllowInput(prompt); userInputActive = true; try { - Character inputMask = secure ? SECURE_MASK_CHAR : defaultInputMask; - return reader.readLine(prompt, inputMask); - } catch (IOException e) { - throw new RuntimeException("Error reading input: " + e.getMessage()); + if (secure) { + return reader.readLine(prompt, SECURE_MASK_CHAR); + } else { + return reader.readLine(prompt, defaultInputMask, (String) null); + } + } catch (org.jline.reader.UserInterruptException e) { + return null; + } catch (org.jline.reader.EndOfFileException e) { + return null; } finally { userInputActive = false; } @@ -1041,4 +1033,12 @@ public class GrailsConsole implements ConsoleLogger { public void setDefaultInputMask(Character defaultInputMask) { this.defaultInputMask = defaultInputMask; } + + /** + * Gets the history for the LineReader + * @return the history + */ + public History getHistory() { + return history; + } } diff --git a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java index a9142afb55..d2fe51eeb1 100644 --- a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java +++ b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java @@ -20,8 +20,8 @@ package grails.build.logging; import java.io.IOException; -import jline.Terminal; -import jline.UnsupportedTerminal; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; /** * This class is meant to keep changes made in support of Eclipse separate from @@ -73,8 +73,10 @@ public class GrailsEclipseConsole extends GrailsConsole { } @Override - protected Terminal createTerminal() { - // unix or windows terminal have no relation at all to the behavior of an Eclipse console. - return new UnsupportedTerminal(); + protected Terminal createTerminal() throws IOException { + // For Eclipse, create a dumb terminal that doesn't try to interact with the console + return TerminalBuilder.builder() + .dumb(true) + .build(); } } diff --git a/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java b/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java index 2b9ff8abb4..564ac5a5cc 100644 --- a/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java +++ b/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java @@ -18,76 +18,62 @@ */ package org.grails.build.interactive; -import java.io.IOException; import java.util.List; -import jline.console.ConsoleReader; -import jline.console.CursorBuffer; -import jline.console.completer.CompletionHandler; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; /** - * Fixes issues with the default CandidateListCompletionHandler such as clearing out the whole buffer when - * a completion matches a list of candidates + * A Completer implementation that wraps candidate list completion behavior. + * In JLine 3, completion handling is integrated into the LineReader itself, + * so this class now acts as a utility completer that can be composed with others. * * @author Graeme Rocher * @since 2.0 */ -public class CandidateListCompletionHandler implements CompletionHandler { +public class CandidateListCompletionHandler implements Completer { + private final Completer delegate; private boolean eagerNewlines = true; - public void setAlwaysIncludeNewline(boolean eagerNewlines) { - this.eagerNewlines = eagerNewlines; + public CandidateListCompletionHandler() { + this.delegate = null; } - public boolean complete(ConsoleReader reader, @SuppressWarnings("rawtypes") List<CharSequence> candidates, int pos) throws IOException { - CursorBuffer buf = reader.getCursorBuffer(); - - // if there is only one completion, then fill in the buffer - if (candidates.size() == 1) { - String value = candidates.get(0).toString(); - - // fail if the only candidate is the same as the current buffer - if (value.equals(buf.toString())) { - return false; - } - - jline.console.completer.CandidateListCompletionHandler.setBuffer(reader, value, pos); - - return true; - } - - if (candidates.size() > 1) { - String value = getUnambiguousCompletions(candidates); + public CandidateListCompletionHandler(Completer delegate) { + this.delegate = delegate; + } - jline.console.completer.CandidateListCompletionHandler.setBuffer(reader, value, pos); - } + public void setAlwaysIncludeNewline(boolean eagerNewlines) { + this.eagerNewlines = eagerNewlines; + } - if (eagerNewlines) { - reader.println(); + @Override + public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + if (delegate != null) { + delegate.complete(reader, line, candidates); } - jline.console.completer.CandidateListCompletionHandler.printCandidates(reader, candidates); - - // redraw the current console buffer - reader.drawLine(); - - return true; } /** * Returns a root that matches all the {@link String} elements * of the specified {@link List}, or null if there are - * no commalities. For example, if the list contains + * no commonalities. For example, if the list contains * <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the * method will return <i>foob</i>. */ - private final String getUnambiguousCompletions(final List<?> candidates) { + public static String getUnambiguousCompletions(final List<Candidate> candidates) { if (candidates == null || candidates.isEmpty()) { return null; } // convert to an array for speed - String[] strings = candidates.toArray(new String[candidates.size()]); + String[] strings = new String[candidates.size()]; + for (int i = 0; i < candidates.size(); i++) { + strings[i] = candidates.get(i).value(); + } String first = strings[0]; StringBuilder candidate = new StringBuilder(); @@ -107,7 +93,7 @@ public class CandidateListCompletionHandler implements CompletionHandler { * @return true is all the elements of <i>candidates</i> * start with <i>starts</i> */ - private final boolean startsWith(final String starts, final String[] candidates) { + private static boolean startsWith(final String starts, final String[] candidates) { for (int i = 0; i < candidates.length; i++) { if (!candidates[i].startsWith(starts)) { return false; diff --git a/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy b/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy index dd970e5479..4f99dd9650 100644 --- a/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy +++ b/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy @@ -18,7 +18,10 @@ */ package grails.build.logging -import jline.console.ConsoleReader +import org.jline.reader.LineReader +import org.jline.reader.LineReaderBuilder +import org.jline.terminal.Terminal +import org.jline.terminal.TerminalBuilder import org.fusesource.jansi.Ansi import spock.lang.IgnoreIf import spock.lang.Issue @@ -48,20 +51,27 @@ class GrailsConsoleSpec extends Specification { PrintStream out GrailsConsole console String output + Terminal terminal def setup() { - InputStream systemIn = Mock(InputStream) - systemIn.read(* _) >> -1 out = Mock(PrintStream) + terminal = TerminalBuilder.builder().dumb(true).build() console = GrailsConsole.getInstance() console.ansiEnabled = true console.out = out - console.reader = new ConsoleReader(systemIn, out) + console.@terminal = terminal + console.@reader = LineReaderBuilder.builder() + .terminal(terminal) + .build() output = "" } + def cleanup() { + terminal?.close() + } + @Issue('GRAILS-10753') def "outputMessage - verify the reset marker at the end of the output"() { when: diff --git a/grails-console/build.gradle b/grails-console/build.gradle index 74778ffc97..0308475832 100644 --- a/grails-console/build.gradle +++ b/grails-console/build.gradle @@ -41,6 +41,8 @@ dependencies { api 'org.apache.groovy:groovy-swing' api 'org.apache.groovy:groovy-groovysh' implementation 'org.fusesource.jansi:jansi' + implementation 'org.jline:jline' + // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3) implementation 'jline:jline' implementation 'net.java.dev.jna:jna' diff --git a/grails-controllers/build.gradle b/grails-controllers/build.gradle index 916444c72d..404ebab570 100644 --- a/grails-controllers/build.gradle +++ b/grails-controllers/build.gradle @@ -47,7 +47,7 @@ dependencies { runtimeOnly project(':grails-i18n') - testRuntimeOnly 'jline:jline' + testRuntimeOnly 'org.jline:jline' testRuntimeOnly 'org.fusesource.jansi:jansi' compileOnly 'jakarta.servlet:jakarta.servlet-api' diff --git a/grails-forge/gradle.properties b/grails-forge/gradle.properties index e0adeeda0b..4b04b11a4f 100644 --- a/grails-forge/gradle.properties +++ b/grails-forge/gradle.properties @@ -35,7 +35,7 @@ groovyVersion=3.0.25 jacksonDatabindVersion=2.18.3 jakartaInjectVersion=1.0.5 # match the jansi version in grails-bom -jansiVersion=1.18 +jansiVersion=2.4.1 javaDiffUtils=4.15 jgitVersion=6.10.0.202406032230-r logbackClassicVersion=1.5.17 diff --git a/grails-gradle/gradle/docs-config.gradle b/grails-gradle/gradle/docs-config.gradle index be2aaf4f1c..685a00fd5d 100644 --- a/grails-gradle/gradle/docs-config.gradle +++ b/grails-gradle/gradle/docs-config.gradle @@ -29,6 +29,7 @@ configurations.register('documentation') { dependencies { add('documentation', platform(project(':grails-gradle-bom'))) add('documentation', 'org.fusesource.jansi:jansi') + // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3) add('documentation', 'jline:jline') add('documentation', 'com.github.javaparser:javaparser-core') add('documentation', "org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}") diff --git a/grails-gradle/model/build.gradle b/grails-gradle/model/build.gradle index 7df57487ca..13203d7e3d 100644 --- a/grails-gradle/model/build.gradle +++ b/grails-gradle/model/build.gradle @@ -58,7 +58,8 @@ dependencies { // impl: org.springframework.boot.env.YamlPropertySourceLoader } - compileOnly 'jline:jline' // for profile compilation + compileOnly 'org.jline:jline' // for profile compilation + compileOnly 'org.fusesource.jansi:jansi' api 'org.yaml:snakeyaml' diff --git a/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy b/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy index ddef7afb50..bf56218e0c 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy +++ b/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy @@ -22,7 +22,7 @@ import groovy.transform.Canonical import groovy.transform.CompileDynamic import groovy.transform.CompileStatic -import jline.console.completer.Completer +import org.jline.reader.Completer /** * Describes a {@link Command} diff --git a/grails-scaffolding/build.gradle b/grails-scaffolding/build.gradle index eb76fa3d09..ec73e3d58f 100644 --- a/grails-scaffolding/build.gradle +++ b/grails-scaffolding/build.gradle @@ -41,7 +41,8 @@ dependencies { api project(':grails-fields') api project(':grails-rest-transforms') - compileOnly 'jline:jline' + compileOnly 'org.jline:jline' + compileOnly 'org.fusesource.jansi:jansi' testImplementation 'org.spockframework:spock-core' testImplementation project(':grails-web-gsp') diff --git a/grails-shell-cli/build.gradle b/grails-shell-cli/build.gradle index a66c828e65..894c55380e 100644 --- a/grails-shell-cli/build.gradle +++ b/grails-shell-cli/build.gradle @@ -54,7 +54,7 @@ dependencies { api 'org.apache.grails.gradle:grails-gradle-model' api 'org.apache.ant:ant' api 'org.fusesource.jansi:jansi' - api 'jline:jline' + api 'org.jline:jline' api "org.gradle:gradle-tooling-api:$gradleToolingApiVersion" compileOnly 'org.springframework.boot:spring-boot' diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy index 0cd30756e2..da19b47e46 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy @@ -25,11 +25,14 @@ import java.util.concurrent.Future import groovy.transform.Canonical import groovy.transform.CompileStatic -import jline.UnixTerminal -import jline.console.UserInterruptException -import jline.console.completer.ArgumentCompleter -import jline.console.completer.Completer -import jline.internal.NonBlockingInputStream +import org.jline.reader.Completer +import org.jline.reader.EndOfFileException +import org.jline.reader.LineReader +import org.jline.reader.LineReaderBuilder +import org.jline.reader.UserInterruptException +import org.jline.reader.impl.completer.AggregateCompleter +import org.jline.reader.impl.completer.ArgumentCompleter +import org.jline.terminal.Terminal import org.gradle.tooling.BuildActionExecuter import org.gradle.tooling.BuildCancelledException import org.gradle.tooling.ProjectConnection @@ -246,8 +249,8 @@ class GrailsCli { // force resolve of all profiles profileRepository.getAllProfiles() def commandNames = CommandRegistry.instance.findCommands(profileRepository).collect() { Command cmd -> cmd.name } - console.reader.addCompleter(new StringsCompleter(commandNames)) - console.reader.addCompleter(new CommandCompleter(CommandRegistry.instance.findCommands(profileRepository))) + console.addCompleter(new StringsCompleter(commandNames)) + console.addCompleter(new CommandCompleter(CommandRegistry.instance.findCommands(profileRepository))) profile = [handleCommand: { ExecutionContext context -> def cl = context.commandLine @@ -394,8 +397,6 @@ class GrailsCli { System.setProperty(Environment.INTERACTIVE_MODE_ENABLED, 'true') GrailsConsole console = projectContext.console - def consoleReader = console.reader - consoleReader.setHandleUserInterrupt(true) def completers = aggregateCompleter.getCompleters() console.resetCompleters() @@ -405,7 +406,7 @@ class GrailsCli { ) completers.addAll((profile.getCompleters(projectContext) ?: []) as Collection<Completer>) - consoleReader.addCompleter(aggregateCompleter) + console.addCompleter(aggregateCompleter) return console } @@ -420,7 +421,6 @@ class GrailsCli { } private void interactiveModeLoop(GrailsConsole console, ExecutorService commandExecutor) { - NonBlockingInputStream nonBlockingInput = (NonBlockingInputStream) console.reader.getInput() interactiveModeActive = true boolean firstRun = true while (keepRunning) { @@ -434,48 +434,34 @@ class GrailsCli { // CTRL-D was pressed, exit interactive mode exitInteractiveMode() } else if (commandLine.trim()) { - if (nonBlockingInput.isNonBlockingEnabled()) { - handleCommandWithCancellationSupport(console, commandLine, commandExecutor, nonBlockingInput) - } else { - handleCommand(cliParser.parseString(commandLine)) - } + handleCommandWithCancellationSupport(console, commandLine, commandExecutor) } } catch (BuildCancelledException cancelledException) { console.updateStatus('Build stopped.') } catch (UserInterruptException e) { exitInteractiveMode() + } catch (EndOfFileException e) { + exitInteractiveMode() } catch (Throwable e) { console.error("Caught exception ${e.message}", e) } } } - private Boolean handleCommandWithCancellationSupport(GrailsConsole console, String commandLine, ExecutorService commandExecutor, NonBlockingInputStream nonBlockingInput) { + private Boolean handleCommandWithCancellationSupport(GrailsConsole console, String commandLine, ExecutorService commandExecutor) { ExecutionContext executionContext = createExecutionContext(cliParser.parseString(commandLine)) Future<?> commandFuture = commandExecutor.submit({ handleCommand(executionContext) } as Callable<Boolean>) - def terminal = console.reader.terminal - if (terminal instanceof UnixTerminal) { - ((UnixTerminal) terminal).disableInterruptCharacter() - } + def terminal = console.terminal try { while (!commandFuture.done) { - if (nonBlockingInput.nonBlockingEnabled) { - int peeked = nonBlockingInput.peek(100L) - if (peeked > 0) { - // read peeked character from buffer - nonBlockingInput.read(1L) - if (peeked == KEYPRESS_CTRL_C || peeked == KEYPRESS_ESC) { - executionContext.console.log(' ') - executionContext.console.updateStatus('Stopping build. Please wait...') - executionContext.cancel() - } - } - } - } - } finally { - if (terminal instanceof UnixTerminal) { - ((UnixTerminal) terminal).enableInterruptCharacter() + // In JLine 3, we handle interrupts differently + // The terminal handles CTRL+C via signal handling + Thread.sleep(100) } + } catch (InterruptedException e) { + executionContext.console.log(' ') + executionContext.console.updateStatus('Stopping build. Please wait...') + executionContext.cancel() } if (!commandFuture.isCancelled()) { try { @@ -625,17 +611,29 @@ class GrailsCli { protected Boolean bang(ExecutionContext context) { def console = context.console - def history = console.reader.history + def history = console.history - //move one step back to ! - history.previous() + if (history == null || history.size() == 0) { + console.error('! not valid. Can not repeat without history') + return false + } + + // Get previous command from history + def historyIterator = history.reverseIterator() + if (!historyIterator.hasNext()) { + console.error('! not valid. Can not repeat without history') + return false + } - if (!history.previous()) { + // Skip the current '!' command + historyIterator.next() + + if (!historyIterator.hasNext()) { console.error('! not valid. Can not repeat without history') + return false } - //another step to previous command - String historicalCommand = history.current() + String historicalCommand = historyIterator.next().line() if (historicalCommand.startsWith('!')) { console.error("Can not repeat command: $historicalCommand") } else { diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy index daa4708ff0..397a7e5c62 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy @@ -20,7 +20,10 @@ package org.grails.cli.gradle.commands import groovy.transform.CompileStatic -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import org.gradle.tooling.BuildLauncher import org.grails.cli.gradle.GradleUtil @@ -65,13 +68,12 @@ class GradleCommand implements ProjectCommand, Completer, ProjectContextAware { } @Override - int complete(String buffer, int cursor, List<CharSequence> candidates) { + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { initializeCompleter() - if (completer) - return completer.complete(buffer, cursor, candidates) - else - return cursor + if (completer) { + completer.complete(reader, line, candidates) + } } private void initializeCompleter() { diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy index 7ccbb5280e..a73248e82e 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy @@ -20,7 +20,10 @@ package org.grails.cli.interactive.completers import groovy.transform.CompileStatic -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine /** * @author Graeme Rocher @@ -38,13 +41,13 @@ class ClosureCompleter implements Completer { Completer getCompleter() { if (completer == null) { - completer = new jline.console.completer.StringsCompleter(closure.call()) + completer = new StringsCompleter(closure.call()) } completer } @Override - int complete(String buffer, int cursor, List<CharSequence> candidates) { - getCompleter().complete(buffer, cursor, candidates) + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + getCompleter().complete(reader, line, candidates) } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy index 68f811bf25..3d8c7e7215 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy @@ -18,10 +18,13 @@ */ package org.grails.cli.interactive.completers -import jline.console.completer.FileNameCompleter +import org.jline.builtins.Completers.FileNameCompleter +import org.jline.reader.Candidate +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine /** - * JLine Completor that does file path matching like FileNameCompletor, + * JLine Completor that does file path matching like FileNameCompleter, * but in addition it escapes whitespace in completions with the '\' * character. * @@ -31,19 +34,22 @@ import jline.console.completer.FileNameCompleter class EscapingFileNameCompletor extends FileNameCompleter { /** - * <p>Gets FileNameCompletor to create a list of candidates and then + * <p>Gets FileNameCompleter to create a list of candidates and then * inserts '\' before any whitespace characters in each of the candidates. * If a candidate ends in a whitespace character, then that is <em>not</em> * escaped.</p> */ - int complete(String buffer, int cursor, List candidates) { - int retval = super.complete(buffer, cursor, candidates) + @Override + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + List<Candidate> tempCandidates = [] + super.complete(reader, line, tempCandidates) - int count = candidates.size() - for (int i = 0; i < count; i++) { - candidates[i] = candidates[i].replaceAll(/(\s)(?!$)/, '\\\\$1') + for (Candidate candidate : tempCandidates) { + String value = candidate.value() + // Escape whitespace in the value, except for trailing whitespace + String escapedValue = value.replaceAll(/(\s)(?!$)/, '\\\\$1') + candidates.add(new Candidate(escapedValue, candidate.displ(), candidate.group(), + candidate.descr(), candidate.suffix(), candidate.key(), candidate.complete())) } - - return retval } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy index abba20c368..f8486b89f7 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy @@ -20,7 +20,10 @@ package org.grails.cli.interactive.completers import java.util.regex.Pattern -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine /** * JLine Completor that accepts a string if it matches a given regular @@ -44,17 +47,16 @@ class RegexCompletor implements Completer { /** * <p>Check whether the whole buffer matches the configured pattern. * If it does, the buffer is added to the <tt>candidates</tt> list - * (which indicates acceptance of the buffer string) and returns 0, - * i.e. the start of the buffer. This mimics the behaviour of SimpleCompletor. + * (which indicates acceptance of the buffer string). * </p> - * <p>If the buffer doesn't match the configured pattern, this returns - * -1 and the <tt>candidates</tt> list is left empty.</p> + * <p>If the buffer doesn't match the configured pattern, the + * <tt>candidates</tt> list is left empty.</p> */ - int complete(String buffer, int cursor, List candidates) { + @Override + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + String buffer = line.word() if (buffer ==~ pattern) { - candidates << buffer - return 0 + candidates.add(new Candidate(buffer)) } - else return -1 } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy index 34829f47c0..cffa8f29ef 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy @@ -18,8 +18,10 @@ */ package org.grails.cli.interactive.completers -import jline.console.completer.Completer -import jline.console.completer.StringsCompleter +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine /** * JLine Completor that mixes a fixed set of options with file path matches. @@ -30,8 +32,8 @@ import jline.console.completer.StringsCompleter */ class SimpleOrFileNameCompletor implements Completer { - private simpleCompletor - private fileNameCompletor + private Completer simpleCompletor + private Completer fileNameCompletor SimpleOrFileNameCompletor(List fixedOptions) { this(fixedOptions as String[]) @@ -42,20 +44,17 @@ class SimpleOrFileNameCompletor implements Completer { fileNameCompletor = new EscapingFileNameCompletor() } - int complete(String buffer, int cursor, List candidates) { + @Override + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { // Try the simple completor first... - def retval = simpleCompletor.complete(buffer, cursor, candidates) + List<Candidate> simpleCandidates = [] + simpleCompletor.complete(reader, line, simpleCandidates) + candidates.addAll(simpleCandidates) // ...and then the file path completor. By using the given candidate // list with both completors we aggregate the results automatically. - def fileRetval = fileNameCompletor.complete(buffer, cursor, candidates) - - // If the simple completor has matched, we return its value, otherwise - // we return whatever the file path matcher returned. This ensures that - // both simple completor and file path completor candidates appear - // correctly in the command prompt. If neither competors have matches, - // we of course return -1. - if (retval == -1) retval = fileRetval - return candidates ? retval : -1 + List<Candidate> fileCandidates = [] + fileNameCompletor.complete(reader, line, fileCandidates) + candidates.addAll(fileCandidates) } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java index becaf7f130..73a9b31265 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java @@ -21,24 +21,23 @@ package org.grails.cli.interactive.completers; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedList; +import java.util.Comparator; import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.Objects; -import jline.console.completer.Completer; - -import static jline.internal.Preconditions.checkNotNull; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; /** - * Copied from jline AggregateCompleter - * - * sorts aggregated completions + * An aggregate completer that sorts completion candidates. * + * @author Graeme Rocher + * @since 3.0 */ -public class SortedAggregateCompleter - implements Completer -{ +public class SortedAggregateCompleter implements Completer { + private final List<Completer> completers = new ArrayList<>(); public SortedAggregateCompleter() { @@ -52,7 +51,7 @@ public class SortedAggregateCompleter * @param completers the collection of completers */ public SortedAggregateCompleter(final Collection<Completer> completers) { - checkNotNull(completers); + Objects.requireNonNull(completers); this.completers.addAll(completers); } @@ -77,40 +76,24 @@ public class SortedAggregateCompleter /** * Perform a completion operation across all aggregated completers. - * - * @see Completer#complete(String, int, java.util.List) - * @return the highest completion return value from all completers */ - public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { - // buffer could be null - checkNotNull(candidates); + @Override + public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + Objects.requireNonNull(candidates); - List<Completion> completions = new ArrayList<>(completers.size()); + List<Candidate> allCandidates = new ArrayList<>(); - // Run each completer, saving its completion results - int max = -1; + // Run each completer, collecting candidates for (Completer completer : completers) { - Completion completion = new Completion(candidates); - completion.complete(completer, buffer, cursor); - - // Compute the max cursor position - max = Math.max(max, completion.cursor); - - completions.add(completion); + List<Candidate> completerCandidates = new ArrayList<>(); + completer.complete(reader, line, completerCandidates); + allCandidates.addAll(completerCandidates); } - SortedSet<CharSequence> allCandidates = new TreeSet<>(); - - // Append candidates from completions which have the same cursor position as max - for (Completion completion : completions) { - if (completion.cursor == max) { - allCandidates.addAll(completion.candidates); - } - } + // Sort the candidates by their value + allCandidates.sort(Comparator.comparing(Candidate::value)); candidates.addAll(allCandidates); - - return max; } /** @@ -122,21 +105,4 @@ public class SortedAggregateCompleter "completers=" + completers + '}'; } - - private class Completion - { - public final List<CharSequence> candidates; - - public int cursor; - - public Completion(final List<CharSequence> candidates) { - checkNotNull(candidates); - this.candidates = new LinkedList<>(candidates); - } - - public void complete(final Completer completer, final String buffer, final int cursor) { - checkNotNull(completer); - this.cursor = completer.complete(buffer, cursor, candidates); - } - } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java index 06adbc6015..db83a20b07 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java @@ -21,12 +21,14 @@ package org.grails.cli.interactive.completers; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; -import jline.console.completer.Completer; - -import static jline.internal.Preconditions.checkNotNull; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; /** * A completer that completes based on a collection of Strings @@ -34,9 +36,8 @@ import static jline.internal.Preconditions.checkNotNull; * @author Graeme Rocher * @since 3.0 */ -public class StringsCompleter - implements Completer -{ +public class StringsCompleter implements Completer { + private SortedSet<String> strings = new TreeSet<>(); public StringsCompleter() { @@ -44,7 +45,7 @@ public class StringsCompleter } public StringsCompleter(final Collection<String> strings) { - checkNotNull(strings); + Objects.requireNonNull(strings); getStrings().addAll(strings); } @@ -60,23 +61,23 @@ public class StringsCompleter this.strings = strings; } - public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { - // buffer could be null - checkNotNull(candidates); + @Override + public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + Objects.requireNonNull(candidates); - if (buffer == null) { - candidates.addAll(getStrings()); - } - else { + String buffer = line.word(); + + if (buffer == null || buffer.isEmpty()) { + for (String string : getStrings()) { + candidates.add(new Candidate(string)); + } + } else { for (String match : getStrings().tailSet(buffer)) { if (!match.startsWith(buffer)) { break; } - - candidates.add(match); + candidates.add(new Candidate(match)); } } - - return candidates.isEmpty() ? -1 : 0; } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy index 6b67ce2e62..dd76a780a2 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy @@ -19,8 +19,8 @@ package org.grails.cli.profile import groovy.transform.CompileStatic import groovy.transform.ToString -import jline.console.completer.ArgumentCompleter -import jline.console.completer.Completer +import org.jline.reader.Completer +import org.jline.reader.impl.completer.ArgumentCompleter import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.graph.Dependency import org.eclipse.aether.graph.Exclusion diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java index fe395824b1..275089b904 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java @@ -22,7 +22,7 @@ import java.io.File; import java.util.List; import java.util.Set; -import jline.console.completer.Completer; +import org.jline.reader.Completer; import org.eclipse.aether.graph.Dependency; import org.grails.config.NavigableMap; @@ -129,7 +129,7 @@ public interface Profile { /** * The profile completers * @param context The {@link org.grails.cli.profile.ProjectContext} instance - * @return An {@link java.lang.Iterable} of {@link jline.console.completer.Completer} instances + * @return An {@link java.lang.Iterable} of {@link org.jline.reader.Completer} instances */ Iterable<Completer> getCompleters(ProjectContext context); diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy index 692aed823c..b85435f55d 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy @@ -19,7 +19,10 @@ package org.grails.cli.profile.commands -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import org.grails.build.parsing.CommandLine import org.grails.build.parsing.CommandLineParser @@ -34,13 +37,13 @@ abstract class ArgumentCompletingCommand implements Command, Completer { CommandLineParser cliParser = new CommandLineParser() @Override - final int complete(String buffer, int cursor, List<CharSequence> candidates) { + final void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { def desc = getDescription() - def commandLine = cliParser.parseString(buffer) - return complete(commandLine, desc, candidates, cursor) + def commandLine = cliParser.parseString(line.line()) + complete(commandLine, desc, candidates) } - protected int complete(CommandLine commandLine, CommandDescription desc, List<CharSequence> candidates, int cursor) { + protected void complete(CommandLine commandLine, CommandDescription desc, List<Candidate> candidates) { def invalidOptions = commandLine.undeclaredOptions.keySet().findAll { String str -> desc.getFlag(str.trim()) == null } @@ -54,15 +57,14 @@ abstract class ArgumentCompletingCommand implements Command, Completer { if (lastOption) { def lastArg = lastOption.key if (arg.name.startsWith(lastArg)) { - candidates.add("${argName.substring(lastArg.length())} ".toString()) + candidates.add(new Candidate("${argName.substring(lastArg.length())} ".toString())) } else if (!invalidOptions) { - candidates.add("$flag ".toString()) + candidates.add(new Candidate("$flag ".toString())) } } else { - candidates.add("$flag ".toString()) + candidates.add(new Candidate("$flag ".toString())) } } } - return cursor } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy index 1520a6caf6..b590e2fe07 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy @@ -18,7 +18,10 @@ */ package org.grails.cli.profile.commands -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import org.grails.cli.profile.Command @@ -37,7 +40,8 @@ class CommandCompleter implements Completer { } @Override - int complete(String buffer, int cursor, List<CharSequence> candidates) { + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + String buffer = line.line() def cmd = commands.find() { def trimmed = buffer.trim() if (trimmed.split(/\s/).size() > 1) { @@ -48,8 +52,7 @@ class CommandCompleter implements Completer { } } if (cmd instanceof Completer) { - return ((Completer) cmd).complete(buffer, cursor, candidates) + ((Completer) cmd).complete(reader, line, candidates) } - return cursor } } diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy index 1766cf4ab7..8747f5d3c2 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy @@ -101,7 +101,7 @@ class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepos } @Override - protected int complete(CommandLine commandLine, CommandDescription desc, List<CharSequence> candidates, int cursor) { + protected void complete(CommandLine commandLine, CommandDescription desc, List<org.jline.reader.Candidate> candidates) { def lastOption = commandLine.lastOption() if (lastOption != null) { // if value == true it means no profile is specified and only the flag is present @@ -109,8 +109,8 @@ class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepos if (lastOption.key == PROFILE_FLAG) { def val = lastOption.value if (val == true) { - candidates.addAll(profileNames) - return cursor + profileNames.each { candidates.add(new org.jline.reader.Candidate(it)) } + return } else if (!profileNames.contains(val)) { def valStr = val.toString() @@ -119,24 +119,24 @@ class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepos }.collect() { String pn -> "${pn.substring(valStr.size())} ".toString() } - candidates.addAll(candidateProfiles) - return cursor + candidateProfiles.each { candidates.add(new org.jline.reader.Candidate(it)) } + return } } else if (lastOption.key == FEATURES_FLAG) { def val = lastOption.value def profile = profileRepository.getProfile(commandLine.hasOption(PROFILE_FLAG) ? commandLine.optionValue(PROFILE_FLAG).toString() : getDefaultProfile()) def featureNames = profile.features.collect() { Feature f -> f.name } if (val == true) { - candidates.addAll(featureNames) - return cursor + featureNames.each { candidates.add(new org.jline.reader.Candidate(it)) } + return } else if (!profileNames.contains(val)) { def valStr = val.toString() if (valStr.endsWith(',')) { def specified = valStr.split(',') - candidates.addAll(featureNames.findAll { String f -> + featureNames.findAll { String f -> !specified.contains(f) - }) - return cursor + }.each { candidates.add(new org.jline.reader.Candidate(it)) } + return } def candidatesFeatures = featureNames.findAll { String pn -> @@ -144,12 +144,12 @@ class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepos }.collect() { String pn -> "${pn.substring(valStr.size())} ".toString() } - candidates.addAll(candidatesFeatures) - return cursor + candidatesFeatures.each { candidates.add(new org.jline.reader.Candidate(it)) } + return } } } - return super.complete(commandLine, desc, candidates, cursor) + super.complete(commandLine, desc, candidates) } protected File getDestinationDirectory(File srcFile) { diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy index c5f29586de..30f5d06e49 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy @@ -20,7 +20,7 @@ package org.grails.cli.profile.commands import groovy.transform.CompileDynamic -import jline.console.completer.Completer +import org.jline.reader.Completer import grails.build.logging.GrailsConsole import org.grails.cli.profile.AbstractStep diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy index 698a3cb420..f107c7163e 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy @@ -16,7 +16,10 @@ */ package org.grails.cli.profile.commands -import jline.console.completer.Completer +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import org.grails.build.parsing.CommandLine import org.grails.build.parsing.CommandLineParser @@ -113,20 +116,20 @@ grails [environment]* [target] [arguments]*' } @Override - int complete(String buffer, int cursor, List<CharSequence> candidates) { + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { def allCommands = findAllCommands().collect() { CommandDescription desc -> desc.name } + String buffer = line.word() for (cmd in allCommands) { if (buffer) { if (cmd.startsWith(buffer)) { - candidates << cmd.substring(buffer.size()) + candidates.add(new Candidate(cmd)) } } else { - candidates << cmd + candidates.add(new Candidate(cmd)) } } - return cursor } protected Collection<CommandDescription> findAllCommands() { diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy index 6ebb3f971d..5bff06d6e8 100644 --- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy +++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy @@ -23,8 +23,11 @@ import java.awt.Desktop import groovy.transform.CompileStatic -import jline.console.completer.Completer -import jline.console.completer.FileNameCompleter +import org.jline.builtins.Completers.FileNameCompleter +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import org.grails.cli.profile.CommandDescription import org.grails.cli.profile.ExecutionContext @@ -71,7 +74,7 @@ class OpenCommand implements ProjectCommand, Completer { } @Override - int complete(String buffer, int cursor, List<CharSequence> candidates) { - return new FileNameCompleter().complete(buffer, cursor, candidates) + void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) { + new FileNameCompleter().complete(reader, line, candidates) } } diff --git a/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java b/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java index f72eb8ae2a..a4b9fd8d77 100644 --- a/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java +++ b/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java @@ -18,12 +18,29 @@ */ package org.grails.cli; -import jline.TerminalSupport; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; -public class TestTerminal extends TerminalSupport { - public TestTerminal() { - super(true); - setAnsiSupported(false); - setEchoEnabled(false); +import java.io.IOException; + +/** + * A terminal for testing purposes that creates a dumb terminal. + */ +public class TestTerminal { + + private final Terminal terminal; + + public TestTerminal() throws IOException { + this.terminal = TerminalBuilder.builder() + .dumb(true) + .build(); + } + + public Terminal getTerminal() { + return terminal; + } + + public void close() throws IOException { + terminal.close(); } } diff --git a/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy b/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy index 01800e8234..34c9f12571 100644 --- a/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy +++ b/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy @@ -18,39 +18,47 @@ */ package org.grails.cli.interactive.completers +import org.jline.reader.Candidate +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine import spock.lang.* class RegexCompletorSpec extends Specification { - @Unroll("String '#source' is not matching") + + @Unroll("String '#source' is matching") def "Simple pattern matches"() { given: "a regex completor and an empty candidate list" def completor = new RegexCompletor(/!\w+/) def candidateList = [] + def parsedLine = Stub(ParsedLine) { + word() >> source + } when: "the completor is invoked for a given string" - def retval = completor.complete(source, 0, candidateList) + completor.complete(null, parsedLine, candidateList) - then: "that string is the sole candidate and the return value is 0" + then: "that string is the sole candidate" candidateList.size() == 1 - candidateList[0] == source - retval == 0 + candidateList[0].value() == source where: source << [ "!ls", "!test_stuff" ] } - @Unroll("String '#source' is incorrectly matching") + @Unroll("String '#source' is not matching") def "Non matching strings"() { given: "a regex completor and an empty candidate list" def completor = new RegexCompletor(/!\w+/) def candidateList = [] + def parsedLine = Stub(ParsedLine) { + word() >> source + } when: "the completor is invoked for a given (non-matching) string" - def retval = completor.complete(source, 0, candidateList) + completor.complete(null, parsedLine, candidateList) - then: "the candidate list is empty and the return value is -1" + then: "the candidate list is empty" candidateList.size() == 0 - retval == -1 where: source << [ "!ls ls", "!", "test", "" ] diff --git a/grails-test-core/build.gradle b/grails-test-core/build.gradle index 6d21f9a0e3..ac147627d2 100644 --- a/grails-test-core/build.gradle +++ b/grails-test-core/build.gradle @@ -51,7 +51,7 @@ dependencies { api 'org.apache.groovy:groovy' // command line requirements - api 'jline:jline' + api 'org.jline:jline' api 'org.fusesource.jansi:jansi' // Ant diff --git a/grails-web-url-mappings/build.gradle b/grails-web-url-mappings/build.gradle index 44461d1d85..3b2aa68bf7 100644 --- a/grails-web-url-mappings/build.gradle +++ b/grails-web-url-mappings/build.gradle @@ -46,7 +46,7 @@ dependencies { compileOnly 'org.fusesource.jansi:jansi' testRuntimeOnly 'org.fusesource.jansi:jansi' - compileOnly 'jline:jline' + compileOnly 'org.jline:jline' implementation 'com.github.ben-manes.caffeine:caffeine'
