This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit a94a90e3c39b36573dc01da83c3c41d28bab3594 Author: Paul King <pa...@asert.com.au> AuthorDate: Wed Aug 20 21:15:07 2025 +1000 GROOVY-8162: Update Groovysh to JLine3 (further /doc improvements) --- .../groovy/org/apache/groovy/groovysh/Main.groovy | 8 +- .../groovy/groovysh/jline/GroovyCommands.groovy | 107 +++++++++++++++++++++ .../groovysh/jline/GroovyConsoleEngine.groovy | 2 +- .../apache/groovy/groovysh/util/DocFinder.groovy | 25 ++++- 4 files changed, 135 insertions(+), 7 deletions(-) diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy index 3ca95a99fd..ffaa9a217c 100644 --- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy +++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy @@ -363,7 +363,9 @@ class Main { // ScriptEngine and command registries GroovyEngine scriptEngine = new GroovyEngine() scriptEngine.put('ROOT', rootURL.toString()) - scriptEngine.put('CONSOLE_OPTIONS', [:]) + if (!scriptEngine.hasVariable('CONSOLE_OPTIONS')) { + scriptEngine.put('CONSOLE_OPTIONS', [:]) + } def interpreterMode = Boolean.parseBoolean(System.getProperty("groovysh.interpreterMode", "true")) scriptEngine.put('GROOVYSH_OPTIONS', [interpreterMode: interpreterMode]) Printer printer = new DefaultPrinter(scriptEngine, configPath) @@ -436,9 +438,9 @@ class Main { KeyMap<Binding> keyMap = reader.keyMaps.get("main") keyMap.bind(new Reference(Widgets.TAILTIP_TOGGLE), KeyMap.alt("s")) keyMap.bind(new Reference(Widgets.AUTOSUGGEST_TOGGLE), KeyMap.alt("v")) - def init = configPath.getUserConfig('groovysh_init') + def init = configPath.getUserConfig('groovysh_init.groovy') if (init) { - systemRegistry.setConsoleOption() // initialize(configPath.getUserConfig('groovysh_init').toFile()) + systemRegistry.initialize(init.toFile()) } if (options.q) { diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy index a4f23ca4a6..1a6f630c22 100644 --- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy +++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyCommands.groovy @@ -43,6 +43,7 @@ import org.jline.reader.impl.completer.NullCompleter import org.jline.reader.impl.completer.StringsCompleter import org.jline.utils.AttributedString +import java.awt.Desktop import java.awt.event.ActionListener import java.lang.reflect.Method import java.nio.charset.Charset @@ -64,6 +65,7 @@ class GroovyCommands extends JlineCommandRegistry implements CommandRegistry { private final Map<String, Tuple4<Function, Function, Function, List<String>>> commands = [ '/inspect' : new Tuple4<>(this::inspect, this::inspectCompleter, this::inspectCmdDesc, ['display/browse object info on terminal/object browser']), '/console' : new Tuple4<>(this::console, this::defCompleter, this::defCmdDesc, ['launch Groovy console']), + '/doc' : new Tuple4<>(this::doc, this::defCompleter, this::defCmdDesc, ['display documentation']), '/grab' : new Tuple4<>(this::grab, this::grabCompleter, this::grabCmdDesc, ['add maven repository dependencies to classpath']), '/classloader' : new Tuple4<>(this::classLoader, this::classloaderCompleter, this::classLoaderCmdDesc, ['display/manage Groovy classLoader data']), '/imports' : new Tuple4<>(this::importsCommand, this::importsCompleter, this::nameDeleteCmdDesc, ['show/delete import statements']), @@ -231,6 +233,111 @@ class GroovyCommands extends JlineCommandRegistry implements CommandRegistry { loadFile(engine, workDir.get().resolve(arg).toFile(), merge) } + private static final String VAR_CONSOLE_OPTIONS = "CONSOLE_OPTIONS" + + private Map<String, Object> consoleOption(String key) { + def opts = engine.hasVariable(VAR_CONSOLE_OPTIONS) + ? (Map<String, Object>) engine.get(VAR_CONSOLE_OPTIONS) + : new HashMap<>() + opts[key] + } + + def doc(CommandInput input) { + def usage = new String[]{ + "/doc - open document on browser", + "Usage: /doc [OBJECT]", + " -? --help Displays command help" + } + try { + parseOptions(usage, input.xargs()); + if (input.xargs().length == 0) { + return null + } + if (!Desktop.isDesktopSupported()) { + throw new IllegalStateException("Desktop is not supported!") + } + Map<String, Object> docs + try { + docs = consoleOption("docs") + } catch (Exception e) { + Exception exception = new IllegalStateException("Bad documents configuration!") + exception.addSuppressed(e) + throw exception + } + if (docs == null) { + throw new IllegalStateException("No documents configuration!") + } + boolean done = false + Object arg = input.xargs()[0] + if (arg instanceof String) { + def addresses = [] + addresses += docs.get(input.args()[0]) + addresses.each { address -> + if (address != null) { + done = true + if (urlExists(address)) { + Desktop.getDesktop().browse(new URI(address)) + } else { + throw new IllegalArgumentException("Document not found: " + address) + } + } + } + } + if (!done) { + String name + if (arg instanceof String && ((String) arg).matches("([a-z]+\\.)+[A-Z][a-zA-Z]+")) { + name = (String) arg + } else { + name = arg.getClass().getCanonicalName() + } + name = name.replaceAll("\\.", "/") + ".html" + Object doc = null + for (Map.Entry<String, Object> entry : docs.entrySet()) { + if (name.matches(entry.getKey())) { + doc = entry.getValue() + break + } + } + if (doc == null) { + throw new IllegalArgumentException("No document configuration for " + name) + } + String url = name + if (doc instanceof Collection) { + for (Object o : (Collection<?>) doc) { + url = o + name + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)) + done = true + } + } + } else { + url = doc + name + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)) + done = true + } + } + if (!done) { + throw new IllegalArgumentException("Document not found: " + url) + } + } + } catch (Exception e) { + saveException(e) + } + return null + } + + private boolean urlExists(String weburl) { + try { + URL url = URI.create(weburl).toURL() + HttpURLConnection huc = (HttpURLConnection) url.openConnection() + huc.setRequestMethod("HEAD") + return huc.getResponseCode() == HttpURLConnection.HTTP_OK + } catch (Exception ignore) { + return false + } + } + def slurpcmd(CommandInput input) { checkArgCount(input, [0, 1, 2, 3, 4]) if (maybePrintHelp(input, '/slurp')) return diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy index 15cad9ebb1..4c1f938888 100644 --- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy +++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyConsoleEngine.groovy @@ -31,7 +31,7 @@ class GroovyConsoleEngine extends ConsoleEngineImpl { private final Printer printer GroovyConsoleEngine(ScriptEngine engine, Printer printer, Supplier<Path> workDir, ConfigurationPath configPath, LineReader reader) { - super(Command.values().toSet() - Command.SLURP, engine, printer, workDir, configPath) + super(Command.values().toSet() - Command.SLURP - Command.DOC, engine, printer, workDir, configPath) this.printer = printer setLineReader(reader) commandNames().each{ name -> rename(Command."${name.toUpperCase()}", "/$name") } diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/util/DocFinder.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/util/DocFinder.groovy index 83fb135a9d..c77a560a41 100644 --- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/util/DocFinder.groovy +++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/util/DocFinder.groovy @@ -18,11 +18,25 @@ */ package org.apache.groovy.groovysh.util -// TODO previous version had browseWithAWT as potential fallback if no native browser found. -// It also provided a list of URLs including GDK links. It would be nice to give those as options (or fire them all off). class DocFinder extends HashMap<String, Object> { + private static boolean exists(String url) { + try { + (url.toURL().openConnection() as HttpURLConnection).with { + requestMethod = 'HEAD' + connectTimeout = 5000 + readTimeout = 5000 + return responseCode in 200..399 + } + } catch (Exception e) { + return false + } + } + @Override Object get(Object key) { + if (containsKey(key)) { + return super.get(key) + } def groovyVersion = GroovySystem.getVersion() Class clazz = Eval.me("${key}") def name = clazz.name @@ -32,6 +46,11 @@ class DocFinder extends HashMap<String, Object> { } def majorVersion = System.getProperty('java.version').tokenize('.')[0] def module = clazz.module?.name ?: 'java.base' - "https://docs.oracle.com/en/java/javase/${majorVersion}/docs/api/${module}/${path}".toString() + def result = ["https://docs.oracle.com/en/java/javase/${majorVersion}/docs/api/${module}/${path}".toString()] + def gdk = "https://docs.groovy-lang.org/latest/html/groovy-jdk/${path}" + if (exists(gdk)) { + result << gdk.toString() + } + result } }