8133652: Implement tab-completion for member select expressions
Reviewed-by: jlaskey, attila
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java Fri Aug 14 18:48:26 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java Mon Aug 17 13:17:25 2015 +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<String> keys = new ArrayList<>(Arrays.asList(prefs.keys()));
+ final List<String> keys = new ArrayList<>(Arrays.asList(prefs.keys()));
Collections.sort(keys);
for (String key : keys) {
if (!key.startsWith(HISTORY_LINE_PREFIX))
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java Fri Aug 14 18:48:26 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java Mon Aug 17 13:17:25 2015 +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<CharSequence> 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, "<suggestions>");
+ } 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>", code, null);
+ final List<? extends Tree> 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<Tree, Void>() {
+ @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;
+ }
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java Mon Aug 17 13:17:25 2015 +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<Object, List<String>> propsCache = new WeakHashMap<>();
+
+ // returns the list of properties of the given object
+ static List<String> 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<String> 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<String> 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<String> 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<String> getProperties(final Object obj, final String prefix) {
+ assert prefix != null && !prefix.isEmpty();
+ return getProperties(obj).stream()
+ .filter(s -> s.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+}
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java Fri Aug 14 18:48:26 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java Mon Aug 17 13:17:25 2015 +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<String> getProperties(final Object object) {
+ if (object instanceof StaticClass) {
+ // static properties of the given class
+ final Class<?> clazz = ((StaticClass)object).getRepresentedClass();
+ final ArrayList<String> 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<String> 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<String> 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.<String>emptyList();
+ }
+
private static int[] copyArray(final byte[] in) {
final int[] out = new int[in.length];
for(int i = 0; i < in.length; ++i) {
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptObject.java Fri Aug 14 18:48:26 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptObject.java Mon Aug 17 13:17:25 2015 +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<String> keys = new HashSet<>();
+ final Set<String> 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.