8134279: jjs should support multiple line input to complete incomplete code
authorsundar
Mon, 24 Aug 2015 16:25:40 +0530
changeset 32317 2b653e4e7d65
parent 32316 5d7dd8dc7729
child 32318 3279b026c98a
8134279: jjs should support multiple line input to complete incomplete code Reviewed-by: attila, hannesw
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/HistoryObject.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/NashornCompleter.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PackagesHelper.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/resources/Shell.properties
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/HistoryObject.java	Mon Aug 24 09:12:35 2015 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/HistoryObject.java	Mon Aug 24 16:25:40 2015 +0530
@@ -30,6 +30,7 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import jdk.internal.jline.console.history.FileHistory;
 import jdk.internal.jline.console.history.History;
 import jdk.nashorn.api.scripting.AbstractJSObject;
@@ -48,6 +49,7 @@
         s.add("forEach");
         s.add("print");
         s.add("size");
+        s.add("toString");
         props = Collections.unmodifiableSet(s);
     }
 
@@ -68,6 +70,8 @@
                 return (Runnable)this::print;
             case "size":
                 return hist.size();
+            case "toString":
+                return (Supplier<String>)this::toString;
         }
         return UNDEFINED;
     }
@@ -82,7 +86,11 @@
 
     @Override
     public String toString() {
-        return "[object history]";
+        final StringBuilder buf = new StringBuilder();
+        for (History.Entry e : hist) {
+            buf.append(e.value()).append('\n');
+        }
+        return buf.toString();
     }
 
     @Override
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Mon Aug 24 09:12:35 2015 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Mon Aug 24 16:25:40 2015 +0530
@@ -99,11 +99,12 @@
     protected int readEvalPrint(final Context context, final Global global) {
         final ScriptEnvironment env = context.getEnv();
         final String prompt = bundle.getString("shell.prompt");
+        final String prompt2 = bundle.getString("shell.prompt2");
         final PrintWriter err = context.getErr();
         final Global oldGlobal = Context.getGlobal();
         final boolean globalChanged = (oldGlobal != global);
         final PropertiesHelper propsHelper = new PropertiesHelper(env._classpath);
-        final Completer completer = new NashornCompleter(context, global, this, propsHelper);
+        final NashornCompleter completer = new NashornCompleter(context, global, this, propsHelper);
 
         try (final Console in = new Console(System.in, System.out, HIST_FILE, completer)) {
             if (globalChanged) {
@@ -153,7 +154,32 @@
                     continue;
                 }
 
-                evalImpl(context, global, source, err, env._dump_on_error);
+                try {
+                    final Object res = context.eval(global, source, global, "<shell>");
+                    if (res != ScriptRuntime.UNDEFINED) {
+                        err.println(JSType.toString(res));
+                    }
+                } catch (final Exception exp) {
+                    // Is this a ECMAScript SyntaxError at last column (of the single line)?
+                    // If so, it is because parser expected more input but got EOF. Try to
+                    // to more lines from the user (multiline edit support).
+
+                    if (completer.isSyntaxErrorAt(exp, 1, source.length())) {
+                        final String fullSrc = completer.readMoreLines(source, exp, in, prompt2, err);
+
+                        // check if we succeeded in getting complete code.
+                        if (fullSrc != null && !fullSrc.isEmpty()) {
+                            evalImpl(context, global, fullSrc, err, env._dump_on_error);
+                        } // else ignore, error reported already by 'completer.readMoreLines'
+                    } else {
+
+                        // can't read more lines to have parseable/complete code.
+                        err.println(exp);
+                        if (env._dump_on_error) {
+                            exp.printStackTrace(err);
+                        }
+                    }
+                }
             }
         } catch (final Exception e) {
             err.println(e);
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/NashornCompleter.java	Mon Aug 24 09:12:35 2015 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/NashornCompleter.java	Mon Aug 24 16:25:40 2015 +0530
@@ -25,9 +25,12 @@
 
 package jdk.nashorn.tools.jjs;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
 import jdk.internal.jline.console.completer.Completer;
+import jdk.internal.jline.console.UserInterruptException;
 import jdk.nashorn.api.tree.AssignmentTree;
 import jdk.nashorn.api.tree.BinaryTree;
 import jdk.nashorn.api.tree.CompilationUnitTree;
@@ -46,14 +49,21 @@
 import jdk.nashorn.api.tree.Parser;
 import jdk.nashorn.api.scripting.NashornException;
 import jdk.nashorn.tools.PartialParser;
+import jdk.nashorn.internal.objects.NativeSyntaxError;
 import jdk.nashorn.internal.objects.Global;
+import jdk.nashorn.internal.runtime.ECMAException;
 import jdk.nashorn.internal.runtime.Context;
+import jdk.nashorn.internal.runtime.ScriptEnvironment;
 import jdk.nashorn.internal.runtime.ScriptRuntime;
 
-// A simple source completer for nashorn
+/**
+ * A simple source completer for nashorn. Handles code completion for
+ * expressions as well as handles incomplete single line code.
+ */
 final class NashornCompleter implements Completer {
     private final Context context;
     private final Global global;
+    private final ScriptEnvironment env;
     private final PartialParser partialParser;
     private final PropertiesHelper propsHelper;
     private final Parser parser;
@@ -62,9 +72,110 @@
             final PartialParser partialParser, final PropertiesHelper propsHelper) {
         this.context = context;
         this.global = global;
+        this.env = context.getEnv();
         this.partialParser = partialParser;
         this.propsHelper = propsHelper;
-        this.parser = Parser.create();
+        this.parser = createParser(env);
+    }
+
+
+    /**
+     * Is this a ECMAScript SyntaxError thrown for parse issue at the given line and column?
+     *
+     * @param exp Throwable to check
+     * @param line line number to check
+     * @param column column number to check
+     *
+     * @return true if the given Throwable is a ECMAScript SyntaxError at given line, column
+     */
+    boolean isSyntaxErrorAt(final Throwable exp, final int line, final int column) {
+        if (exp instanceof ECMAException) {
+            final ECMAException eexp = (ECMAException)exp;
+            if (eexp.getThrown() instanceof NativeSyntaxError) {
+                return isParseErrorAt(eexp.getCause(), line, column);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Is this a parse error at the given line and column?
+     *
+     * @param exp Throwable to check
+     * @param line line number to check
+     * @param column column number to check
+     *
+     * @return true if the given Throwable is a parser error at given line, column
+     */
+    boolean isParseErrorAt(final Throwable exp, final int line, final int column) {
+        if (exp instanceof NashornException) {
+            final NashornException nexp = (NashornException)exp;
+            return nexp.getLineNumber() == line && nexp.getColumnNumber() == column;
+        }
+        return false;
+    }
+
+
+    /**
+     * Read more lines of code if we got SyntaxError at EOF and we can it fine by
+     * by reading more lines of code from the user. This is used for multiline editing.
+     *
+     * @param firstLine First line of code from the user
+     * @param exp       Exception thrown by evaluting first line code
+     * @param in        Console to get read more lines from the user
+     * @param prompt    Prompt to be printed to read more lines from the user
+     * @param err       PrintWriter to print any errors in the proecess of reading
+     *
+     * @return Complete code read from the user including the first line. This is null
+     *         if any error or the user discarded multiline editing by Ctrl-C.
+     */
+    String readMoreLines(final String firstLine, final Exception exp, final Console in,
+            final String prompt, final PrintWriter err) {
+        int line = 1;
+        final StringBuilder buf = new StringBuilder(firstLine);
+        while (true) {
+            buf.append('\n');
+            String curLine = null;
+            try {
+                curLine = in.readLine(prompt);
+                buf.append(curLine);
+                line++;
+            } catch (final Throwable th) {
+                if (th instanceof UserInterruptException) {
+                    // Ctrl-C from user - discard the whole thing silently!
+                    return null;
+                } else {
+                    // print anything else -- but still discard the code
+                    err.println(th);
+                    if (env._dump_on_error) {
+                        th.printStackTrace(err);
+                    }
+                    return null;
+                }
+            }
+
+            final String allLines = buf.toString();
+            try {
+                parser.parse("<shell>", allLines, null);
+            } catch (final Exception pexp) {
+                // Do we have a parse error at the end of current line?
+                // If so, read more lines from the console.
+                if (isParseErrorAt(pexp, line, curLine.length())) {
+                    continue;
+                } else {
+                    // print anything else and bail out!
+                    err.println(pexp);
+                    if (env._dump_on_error) {
+                        pexp.printStackTrace(err);
+                    }
+                    return null;
+                }
+            }
+
+            // We have complete parseable code!
+            return buf.toString();
+        }
     }
 
     // Pattern to match a unfinished member selection expression. object part and "."
@@ -116,6 +227,9 @@
         }
     }
 
+    // Internals only below this point
+
+    // fill properties of the incomplete member expression
     private int completeMemberSelect(final String exprStr, final int cursor, final List<CharSequence> result,
                 final MemberSelectTree select, final boolean endsWithDot) {
         final ExpressionTree objExpr = select.getExpression();
@@ -148,6 +262,7 @@
         return cursor;
     }
 
+    // fill properties for the given (partial) identifer
     private int completeIdentifier(final String test, final int cursor, final List<CharSequence> result,
                 final IdentifierTree ident) {
         final String name = ident.getName();
@@ -175,6 +290,7 @@
         return null;
     }
 
+    // get the right most expreesion of the given expression
     private Tree getRightMostExpression(final ExpressionTree expr) {
         return expr.accept(new SimpleTreeVisitorES5_1<Tree, Void>() {
             @Override
@@ -234,4 +350,27 @@
             }
         }, null);
     }
+
+    // create a Parser instance that uses compatible command line options of the
+    // current ScriptEnvironment being used for REPL.
+    private static Parser createParser(final ScriptEnvironment env) {
+        final List<String> args = new ArrayList<>();
+        if (env._const_as_var) {
+            args.add("--const-as-var");
+        }
+
+        if (env._no_syntax_extensions) {
+            args.add("-nse");
+        }
+
+        if (env._scripting) {
+            args.add("-scripting");
+        }
+
+        if (env._strict) {
+            args.add("-strict");
+        }
+
+        return Parser.create(args.toArray(new String[0]));
+    }
 }
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PackagesHelper.java	Mon Aug 24 09:12:35 2015 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PackagesHelper.java	Mon Aug 24 16:25:40 2015 +0530
@@ -86,7 +86,7 @@
 
     // LRU cache for java package properties lists
     private final LinkedHashMap<String, List<String>> propsCache =
-        new LinkedHashMap<>(32, 0.75f, true) {
+        new LinkedHashMap<String, List<String>>(32, 0.75f, true) {
             private static final int CACHE_SIZE = 100;
             private static final long serialVersionUID = 1;
 
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/resources/Shell.properties	Mon Aug 24 09:12:35 2015 +0200
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/resources/Shell.properties	Mon Aug 24 16:25:40 2015 +0530
@@ -29,4 +29,5 @@
 
 shell.prompt=jjs> 
 
+shell.prompt2=...>