8133652: Implement tab-completion for member select expressions

Reviewed-by: jlaskey, attila
This commit is contained in:
Athijegannathan Sundararajan 2015-08-17 13:17:25 +05:30
parent 67e6d1bad0
commit a45bb1ba66
5 changed files with 316 additions and 5 deletions

View File

@ -36,6 +36,7 @@ import java.util.List;
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 @@ class Console implements AutoCloseable {
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 @@ class Console implements AutoCloseable {
private final Preferences prefs;
protected PersistentHistory(Preferences prefs) {
protected PersistentHistory(final Preferences prefs) {
this.prefs = prefs;
load();
}
@ -74,7 +78,7 @@ class Console implements AutoCloseable {
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))

View File

@ -31,14 +31,32 @@ import java.io.InputStreamReader;
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 @@ public final class Main extends Shell {
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 @@ public final class Main extends Shell {
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;
}
}

View File

@ -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());
}
}

View File

@ -30,11 +30,14 @@ import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
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 @@ public final class NativeJava {
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) {

View File

@ -1339,6 +1339,21 @@ public abstract class ScriptObject implements PropertyAccess, Cloneable {
}
}
/**
* 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.
*