--- old/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java 2015-08-17 12:39:30.733331800 +0530 +++ new/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java 2015-08-17 12:39:30.358310400 +0530 @@ -36,6 +36,7 @@ import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import jdk.internal.jline.console.ConsoleReader; +import jdk.internal.jline.console.completer.Completer; import jdk.internal.jline.console.history.History.Entry; import jdk.internal.jline.console.history.MemoryHistory; @@ -43,15 +44,18 @@ private final ConsoleReader in; private final PersistentHistory history; - Console(InputStream cmdin, PrintStream cmdout, Preferences prefs) throws IOException { + Console(final InputStream cmdin, final PrintStream cmdout, final Preferences prefs, + final Completer completer) throws IOException { in = new ConsoleReader(cmdin, cmdout); in.setExpandEvents(false); in.setHandleUserInterrupt(true); + in.setBellEnabled(true); in.setHistory(history = new PersistentHistory(prefs)); + in.addCompleter(completer); Runtime.getRuntime().addShutdownHook(new Thread(()->close())); } - String readLine(String prompt) throws IOException { + String readLine(final String prompt) throws IOException { return in.readLine(prompt); } @@ -65,7 +69,7 @@ private final Preferences prefs; - protected PersistentHistory(Preferences prefs) { + protected PersistentHistory(final Preferences prefs) { this.prefs = prefs; load(); } @@ -74,7 +78,7 @@ public final void load() { try { - List keys = new ArrayList<>(Arrays.asList(prefs.keys())); + final List keys = new ArrayList<>(Arrays.asList(prefs.keys())); Collections.sort(keys); for (String key : keys) { if (!key.startsWith(HISTORY_LINE_PREFIX)) --- old/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java 2015-08-17 12:39:33.143469700 +0530 +++ new/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java 2015-08-17 12:39:32.756447500 +0530 @@ -31,14 +31,32 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.util.Iterator; +import java.util.List; import java.util.prefs.Preferences; +import jdk.nashorn.api.tree.AssignmentTree; +import jdk.nashorn.api.tree.BinaryTree; +import jdk.nashorn.api.tree.CompilationUnitTree; +import jdk.nashorn.api.tree.CompoundAssignmentTree; +import jdk.nashorn.api.tree.ConditionalExpressionTree; +import jdk.nashorn.api.tree.ExpressionTree; +import jdk.nashorn.api.tree.ExpressionStatementTree; +import jdk.nashorn.api.tree.InstanceOfTree; +import jdk.nashorn.api.tree.MemberSelectTree; +import jdk.nashorn.api.tree.SimpleTreeVisitorES5_1; +import jdk.nashorn.api.tree.Tree; +import jdk.nashorn.api.tree.UnaryTree; +import jdk.nashorn.api.tree.Parser; +import jdk.nashorn.api.scripting.NashornException; import jdk.nashorn.internal.objects.Global; import jdk.nashorn.internal.runtime.Context; import jdk.nashorn.internal.runtime.ErrorManager; import jdk.nashorn.internal.runtime.JSType; import jdk.nashorn.internal.runtime.ScriptEnvironment; +import jdk.nashorn.internal.runtime.ScriptObject; import jdk.nashorn.internal.runtime.ScriptRuntime; import jdk.nashorn.tools.Shell; +import jdk.internal.jline.console.completer.Completer; import jdk.internal.jline.console.UserInterruptException; /** @@ -96,8 +114,72 @@ final PrintWriter err = context.getErr(); final Global oldGlobal = Context.getGlobal(); final boolean globalChanged = (oldGlobal != global); + final Parser parser = Parser.create(); - try (final Console in = new Console(System.in, System.out, PREFS)) { + // simple source "tab completer" for nashorn + final Completer completer = new Completer() { + @Override + public int complete(final String test, final int cursor, final List result) { + // check that cursor is at the end of test string. Do not complete in the middle! + if (cursor != test.length()) { + return cursor; + } + + // if it has a ".", then assume it is a member selection expression + final int idx = test.lastIndexOf('.'); + if (idx == -1) { + return cursor; + } + + // stuff before the last "." + final String exprBeforeDot = test.substring(0, idx); + + // Make sure that completed code will have a member expression! Adding ".x" as a + // random property/field name selected to make it possible to be a proper member select + final ExpressionTree topExpr = getTopLevelExpression(parser, exprBeforeDot + ".x"); + if (topExpr == null) { + // did not parse to be a top level expression, no suggestions! + return cursor; + } + + + // Find 'right most' member select expression's start position + final int startPosition = (int) getStartOfMemberSelect(topExpr); + if (startPosition == -1) { + // not a member expression that we can handle for completion + return cursor; + } + + // The part of the right most member select expression before the "." + final String objExpr = test.substring(startPosition, idx); + + // try to evaluate the object expression part as a script + Object obj = null; + try { + obj = context.eval(global, objExpr, global, ""); + } catch (Exception ignored) { + // throw the exception - this is during tab-completion + } + + if (obj != null && obj != ScriptRuntime.UNDEFINED) { + // where is the last dot? Is there a partial property name specified? + final String prefix = test.substring(idx + 1); + if (prefix.isEmpty()) { + // no user specified "prefix". List all properties of the object + result.addAll(PropertiesHelper.getProperties(obj)); + return cursor; + } else { + // list of properties matching the user specified prefix + result.addAll(PropertiesHelper.getProperties(obj, prefix)); + return idx + 1; + } + } + + return cursor; + } + }; + + try (final Console in = new Console(System.in, System.out, PREFS, completer)) { if (globalChanged) { Context.setGlobal(global); } @@ -147,4 +229,66 @@ return SUCCESS; } + + // returns ExpressionTree if the given code parses to a top level expression. + // Or else returns null. + private ExpressionTree getTopLevelExpression(final Parser parser, final String code) { + try { + final CompilationUnitTree cut = parser.parse("", code, null); + final List stats = cut.getSourceElements(); + if (stats.size() == 1) { + final Tree stat = stats.get(0); + if (stat instanceof ExpressionStatementTree) { + return ((ExpressionStatementTree)stat).getExpression(); + } + } + } catch (final NashornException ignored) { + // ignore any parser error. This is for completion anyway! + // And user will get that error later when the expression is evaluated. + } + + return null; + } + + + private long getStartOfMemberSelect(final ExpressionTree expr) { + if (expr instanceof MemberSelectTree) { + return ((MemberSelectTree)expr).getStartPosition(); + } + + final Tree rightMostExpr = expr.accept(new SimpleTreeVisitorES5_1() { + @Override + public Tree visitAssignment(final AssignmentTree at, final Void v) { + return at.getExpression(); + } + + @Override + public Tree visitCompoundAssignment(final CompoundAssignmentTree cat, final Void v) { + return cat.getExpression(); + } + + @Override + public Tree visitConditionalExpression(final ConditionalExpressionTree cet, final Void v) { + return cet.getFalseExpression(); + } + + @Override + public Tree visitBinary(final BinaryTree bt, final Void v) { + return bt.getRightOperand(); + } + + @Override + public Tree visitInstanceOf(final InstanceOfTree it, final Void v) { + return it.getType(); + } + + @Override + public Tree visitUnary(final UnaryTree ut, final Void v) { + return ut.getExpression(); + } + }, null); + + return (rightMostExpr instanceof MemberSelectTree)? + rightMostExpr.getStartPosition() : -1L; + } } --- old/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java 2015-08-17 12:39:35.715616800 +0530 +++ new/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java 2015-08-17 12:39:35.328594700 +0530 @@ -30,11 +30,14 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Queue; +import jdk.internal.dynalink.beans.BeansLinker; import jdk.internal.dynalink.beans.StaticClass; import jdk.internal.dynalink.support.TypeUtilities; import jdk.nashorn.api.scripting.JSObject; @@ -443,6 +446,47 @@ throw typeError("cant.convert.to.javascript.array", objArray.getClass().getName()); } + /** + * Return properties of the given object. Properties also include "method names". + * This is meant for source code completion in interactive shells or editors. + * + * @param object the object whose properties are returned. + * @return list of properties + */ + public static List getProperties(final Object object) { + if (object instanceof StaticClass) { + // static properties of the given class + final Class clazz = ((StaticClass)object).getRepresentedClass(); + final ArrayList props = new ArrayList<>(); + try { + Bootstrap.checkReflectionAccess(clazz, true); + // Usually writable properties are a subset as 'write-only' properties are rare + props.addAll(BeansLinker.getReadableStaticPropertyNames(clazz)); + props.addAll(BeansLinker.getStaticMethodNames(clazz)); + } catch (Exception ignored) {} + return props; + } else if (object instanceof JSObject) { + final JSObject jsObj = ((JSObject)object); + final ArrayList props = new ArrayList<>(); + props.addAll(jsObj.keySet()); + return props; + } else if (object != null && object != UNDEFINED) { + // instance properties of the given object + final Class clazz = object.getClass(); + final ArrayList props = new ArrayList<>(); + try { + Bootstrap.checkReflectionAccess(clazz, false); + // Usually writable properties are a subset as 'write-only' properties are rare + props.addAll(BeansLinker.getReadableInstancePropertyNames(clazz)); + props.addAll(BeansLinker.getInstanceMethodNames(clazz)); + } catch (Exception ignored) {} + return props; + } + + // don't know about that object + return Collections.emptyList(); + } + private static int[] copyArray(final byte[] in) { final int[] out = new int[in.length]; for(int i = 0; i < in.length; ++i) { --- old/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptObject.java 2015-08-17 12:39:38.232760800 +0530 +++ new/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptObject.java 2015-08-17 12:39:37.845738600 +0530 @@ -1340,6 +1340,21 @@ } /** + * return an array of all property keys - all inherited, non-enumerable included. + * This is meant for source code completion by interactive shells or editors. + * + * @return Array of keys, order of properties is undefined. + */ + public String[] getAllKeys() { + final Set keys = new HashSet<>(); + final Set nonEnumerable = new HashSet<>(); + for (ScriptObject self = this; self != null; self = self.getProto()) { + keys.addAll(Arrays.asList(self.getOwnKeys(true, nonEnumerable))); + } + return keys.toArray(new String[keys.size()]); + } + + /** * return an array of own property keys associated with the object. * * @param all True if to include non-enumerable keys. --- /dev/null 2015-08-17 12:39:40.000000000 +0530 +++ new/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java 2015-08-17 12:39:40.403885000 +0530 @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.nashorn.tools.jjs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.WeakHashMap; +import java.util.stream.Collectors; +import jdk.nashorn.internal.runtime.JSType; +import jdk.nashorn.internal.runtime.PropertyMap; +import jdk.nashorn.internal.runtime.ScriptObject; +import jdk.nashorn.internal.runtime.ScriptRuntime; +import jdk.nashorn.internal.objects.NativeJava; + +/* + * A helper class to get properties of a given object for source code completion. + */ +final class PropertiesHelper { + private PropertiesHelper() {} + + // cached properties list + private static final WeakHashMap> propsCache = new WeakHashMap<>(); + + // returns the list of properties of the given object + static List getProperties(final Object obj) { + assert obj != null && obj != ScriptRuntime.UNDEFINED; + + if (JSType.isPrimitive(obj)) { + return getProperties(JSType.toScriptObject(obj)); + } + + if (obj instanceof ScriptObject) { + final ScriptObject sobj = (ScriptObject)obj; + final PropertyMap pmap = sobj.getMap(); + if (propsCache.containsKey(pmap)) { + return propsCache.get(pmap); + } + final String[] keys = sobj.getAllKeys(); + List props = Arrays.asList(keys); + props = props.stream() + .filter(s -> Character.isJavaIdentifierStart(s.charAt(0))) + .collect(Collectors.toList()); + Collections.sort(props); + // cache properties against the PropertyMap + propsCache.put(pmap, props); + return props; + } + + if (NativeJava.isType(ScriptRuntime.UNDEFINED, obj)) { + if (propsCache.containsKey(obj)) { + return propsCache.get(obj); + } + final List props = NativeJava.getProperties(obj); + Collections.sort(props); + // cache properties against the StaticClass representing the class + propsCache.put(obj, props); + return props; + } + + final Class clazz = obj.getClass(); + if (propsCache.containsKey(clazz)) { + return propsCache.get(clazz); + } + + final List props = NativeJava.getProperties(obj); + Collections.sort(props); + // cache properties against the Class object + propsCache.put(clazz, props); + return props; + } + + // returns the list of properties of the given object that start with the given prefix + static List getProperties(final Object obj, final String prefix) { + assert prefix != null && !prefix.isEmpty(); + return getProperties(obj).stream() + .filter(s -> s.startsWith(prefix)) + .collect(Collectors.toList()); + } +}