8133652: Implement tab-completion for member select expressions
authorsundar
Mon, 17 Aug 2015 13:17:25 +0530
changeset 32240 d7c7a5dc92d8
parent 32239 3d2ad478611b
child 32241 f6228e27ad97
8133652: Implement tab-completion for member select expressions Reviewed-by: jlaskey, attila
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptObject.java
--- 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.