8133948: Add 'edit' function to allow external editing of scripts
authorsundar
Fri, 21 Aug 2015 18:01:23 +0530
changeset 32313 11b284f8c4c6
parent 32256 3966bd3b8167
child 32314 8f7d23d3b1ad
8133948: Add 'edit' function to allow external editing of scripts Reviewed-by: attila, hannesw, jlahoda
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/EditObject.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditPad.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/ExternalEditor.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java	Wed Jul 05 20:46:39 2017 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java	Fri Aug 21 18:01:23 2015 +0530
@@ -34,6 +34,11 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import jdk.internal.jline.NoInterruptUnixTerminal;
+import jdk.internal.jline.Terminal;
+import jdk.internal.jline.TerminalFactory;
+import jdk.internal.jline.TerminalFactory.Flavor;
+import jdk.internal.jline.WindowsTerminal;
 import jdk.internal.jline.console.ConsoleReader;
 import jdk.internal.jline.console.completer.Completer;
 import jdk.internal.jline.console.history.FileHistory;
@@ -45,6 +50,8 @@
     Console(final InputStream cmdin, final PrintStream cmdout, final File historyFile,
             final Completer completer) throws IOException {
         in = new ConsoleReader(cmdin, cmdout);
+        TerminalFactory.registerFlavor(Flavor.WINDOWS, JJSWindowsTerminal :: new);
+        TerminalFactory.registerFlavor(Flavor.UNIX, JJSUnixTerminal :: new);
         in.setExpandEvents(false);
         in.setHandleUserInterrupt(true);
         in.setBellEnabled(true);
@@ -71,4 +78,60 @@
     FileHistory getHistory() {
         return (FileHistory) in.getHistory();
     }
+
+    boolean terminalEditorRunning() {
+        Terminal terminal = in.getTerminal();
+        if (terminal instanceof JJSUnixTerminal) {
+            return ((JJSUnixTerminal) terminal).isRaw();
+        }
+        return false;
+    }
+
+    void suspend() {
+        try {
+            in.getTerminal().restore();
+        } catch (Exception ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    void resume() {
+        try {
+            in.getTerminal().init();
+        } catch (Exception ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    static final class JJSUnixTerminal extends NoInterruptUnixTerminal {
+        JJSUnixTerminal() throws Exception {
+        }
+
+        boolean isRaw() {
+            try {
+                return getSettings().get("-a").contains("-icanon");
+            } catch (IOException | InterruptedException ex) {
+                return false;
+            }
+        }
+
+        @Override
+        public void disableInterruptCharacter() {
+        }
+
+        @Override
+        public void enableInterruptCharacter() {
+        }
+    }
+
+    static final class JJSWindowsTerminal extends WindowsTerminal {
+        public JJSWindowsTerminal() throws Exception {
+        }
+
+        @Override
+        public void init() throws Exception {
+            super.init();
+            setAnsiSupported(false);
+        }
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditObject.java	Fri Aug 21 18:01:23 2015 +0530
@@ -0,0 +1,113 @@
+/*
+ * 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.function.Consumer;
+import jdk.nashorn.api.scripting.AbstractJSObject;
+import jdk.nashorn.internal.runtime.JSType;
+import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
+
+/*
+ * "edit" top level script function which shows an external Window
+ * for editing and evaluating scripts from it.
+ */
+final class EditObject extends AbstractJSObject {
+    private final Consumer<String> errorHandler;
+    private final Consumer<String> evaluator;
+    private final Console console;
+    private String editor;
+
+    EditObject(final Consumer<String> errorHandler, final Consumer<String> evaluator,
+            final Console console) {
+        this.errorHandler = errorHandler;
+        this.evaluator = evaluator;
+        this.console = console;
+    }
+
+    @Override
+    public Object getDefaultValue(final Class<?> hint) {
+        if (hint == String.class) {
+            return toString();
+        }
+        return UNDEFINED;
+    }
+
+    @Override
+    public String toString() {
+        return "function edit() { [native code] }";
+    }
+
+    @Override
+    public Object getMember(final String name) {
+        if (name.equals("editor")) {
+            return editor;
+        }
+        return UNDEFINED;
+    }
+
+    @Override
+    public void setMember(final String name, final Object value) {
+        if (name.equals("editor")) {
+            this.editor = JSType.toString(value);
+        }
+    }
+
+    // called whenever user 'saves' script in editor
+    class SaveHandler implements Consumer<String> {
+         private String lastStr; // last seen code
+
+         SaveHandler(final String str) {
+             this.lastStr = str;
+         }
+
+         @Override
+         public void accept(final String str) {
+             // ignore repeated save of the same code!
+             if (! str.equals(lastStr)) {
+                 this.lastStr = str;
+                 // evaluate the new code
+                 evaluator.accept(str);
+             }
+         }
+    }
+
+    @Override
+    public Object call(final Object thiz, final Object... args) {
+        final String initText = args.length > 0? JSType.toString(args[0]) : "";
+        final SaveHandler saveHandler = new SaveHandler(initText);
+        if (editor != null && !editor.isEmpty()) {
+            ExternalEditor.edit(editor, errorHandler, initText, saveHandler, console);
+        } else {
+            EditPad.edit(errorHandler, initText, saveHandler);
+        }
+        return UNDEFINED;
+    }
+
+    @Override
+    public boolean isFunction() {
+        return true;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditPad.java	Fri Aug 21 18:01:23 2015 +0530
@@ -0,0 +1,136 @@
+/*
+ * 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.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.function.Consumer;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingUtilities;
+
+/**
+ * A minimal Swing editor as a fallback when the user does not specify an
+ * external editor.
+ */
+final class EditPad extends JFrame implements Runnable {
+    private static final long serialVersionUID = 1;
+    private final Consumer<String> errorHandler;
+    private final String initialText;
+    private final boolean[] closeLock;
+    private final Consumer<String> saveHandler;
+
+    EditPad(Consumer<String> errorHandler, String initialText,
+            boolean[] closeLock, Consumer<String> saveHandler) {
+        super("Edit Pad (Experimental)");
+        this.errorHandler = errorHandler;
+        this.initialText = initialText;
+        this.closeLock = closeLock;
+        this.saveHandler = saveHandler;
+    }
+
+    @Override
+    public void run() {
+        addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                EditPad.this.dispose();
+                notifyClose();
+            }
+        });
+        setLocationRelativeTo(null);
+        setLayout(new BorderLayout());
+        JTextArea textArea = new JTextArea(initialText);
+        add(new JScrollPane(textArea), BorderLayout.CENTER);
+        add(buttons(textArea), BorderLayout.SOUTH);
+
+        setSize(800, 600);
+        setVisible(true);
+    }
+
+    private JPanel buttons(JTextArea textArea) {
+        FlowLayout flow = new FlowLayout();
+        flow.setHgap(35);
+        JPanel buttons = new JPanel(flow);
+        JButton cancel = new JButton("Cancel");
+        cancel.setMnemonic(KeyEvent.VK_C);
+        JButton accept = new JButton("Accept");
+        accept.setMnemonic(KeyEvent.VK_A);
+        JButton exit = new JButton("Exit");
+        exit.setMnemonic(KeyEvent.VK_X);
+        buttons.add(cancel);
+        buttons.add(accept);
+        buttons.add(exit);
+
+        cancel.addActionListener(e -> {
+            close();
+        });
+        accept.addActionListener(e -> {
+            saveHandler.accept(textArea.getText());
+        });
+        exit.addActionListener(e -> {
+            saveHandler.accept(textArea.getText());
+            close();
+        });
+
+        return buttons;
+    }
+
+    private void close() {
+        setVisible(false);
+        dispose();
+        notifyClose();
+    }
+
+    private void notifyClose() {
+        synchronized (closeLock) {
+            closeLock[0] = true;
+            closeLock.notify();
+        }
+    }
+
+    static void edit(Consumer<String> errorHandler, String initialText,
+            Consumer<String> saveHandler) {
+        boolean[] closeLock = new boolean[1];
+        SwingUtilities.invokeLater(
+                new EditPad(errorHandler, initialText, closeLock, saveHandler));
+        synchronized (closeLock) {
+            while (!closeLock[0]) {
+                try {
+                    closeLock.wait();
+                } catch (InterruptedException ex) {
+                    // ignore and loop
+                }
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/ExternalEditor.java	Fri Aug 21 18:01:23 2015 +0530
@@ -0,0 +1,152 @@
+/*
+ * 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.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.List;
+import java.util.function.Consumer;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+final class ExternalEditor {
+    private final Consumer<String> errorHandler;
+    private final Consumer<String> saveHandler;
+    private final Console input;
+
+    private WatchService watcher;
+    private Thread watchedThread;
+    private Path dir;
+    private Path tmpfile;
+
+    ExternalEditor(Consumer<String> errorHandler, Consumer<String> saveHandler, Console input) {
+        this.errorHandler = errorHandler;
+        this.saveHandler = saveHandler;
+        this.input = input;
+    }
+
+    private void edit(String cmd, String initialText) {
+        try {
+            setupWatch(initialText);
+            launch(cmd);
+        } catch (IOException ex) {
+            errorHandler.accept(ex.getMessage());
+        }
+    }
+
+    /**
+     * Creates a WatchService and registers the given directory
+     */
+    private void setupWatch(String initialText) throws IOException {
+        this.watcher = FileSystems.getDefault().newWatchService();
+        this.dir = Files.createTempDirectory("REPL");
+        this.tmpfile = Files.createTempFile(dir, null, ".js");
+        Files.write(tmpfile, initialText.getBytes(Charset.forName("UTF-8")));
+        dir.register(watcher,
+                ENTRY_CREATE,
+                ENTRY_DELETE,
+                ENTRY_MODIFY);
+        watchedThread = new Thread(() -> {
+            for (;;) {
+                WatchKey key;
+                try {
+                    key = watcher.take();
+                } catch (ClosedWatchServiceException ex) {
+                    break;
+                } catch (InterruptedException ex) {
+                    continue; // tolerate an intrupt
+                }
+
+                if (!key.pollEvents().isEmpty()) {
+                    if (!input.terminalEditorRunning()) {
+                        saveFile();
+                    }
+                }
+
+                boolean valid = key.reset();
+                if (!valid) {
+                    errorHandler.accept("Invalid key");
+                    break;
+                }
+            }
+        });
+        watchedThread.start();
+    }
+
+    private void launch(String cmd) throws IOException {
+        ProcessBuilder pb = new ProcessBuilder(cmd, tmpfile.toString());
+        pb = pb.inheritIO();
+
+        try {
+            input.suspend();
+            Process process = pb.start();
+            process.waitFor();
+        } catch (IOException ex) {
+            errorHandler.accept("process IO failure: " + ex.getMessage());
+        } catch (InterruptedException ex) {
+            errorHandler.accept("process interrupt: " + ex.getMessage());
+        } finally {
+            try {
+                watcher.close();
+                watchedThread.join(); //so that saveFile() is finished.
+                saveFile();
+            } catch (InterruptedException ex) {
+                errorHandler.accept("process interrupt: " + ex.getMessage());
+            } finally {
+                input.resume();
+            }
+        }
+    }
+
+    private void saveFile() {
+        List<String> lines;
+        try {
+            lines = Files.readAllLines(tmpfile);
+        } catch (IOException ex) {
+            errorHandler.accept("Failure read edit file: " + ex.getMessage());
+            return ;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (String ln : lines) {
+            sb.append(ln);
+            sb.append('\n');
+        }
+        saveHandler.accept(sb.toString());
+    }
+
+    static void edit(String cmd, Consumer<String> errorHandler, String initialText,
+            Consumer<String> saveHandler, Console input) {
+        ExternalEditor ed = new ExternalEditor(errorHandler,  saveHandler, input);
+        ed.edit(cmd, initialText);
+    }
+}
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Wed Jul 05 20:46:39 2017 +0200
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Fri Aug 21 18:01:23 2015 +0530
@@ -38,6 +38,7 @@
 import jdk.nashorn.internal.objects.Global;
 import jdk.nashorn.internal.runtime.Context;
 import jdk.nashorn.internal.runtime.JSType;
+import jdk.nashorn.internal.runtime.Property;
 import jdk.nashorn.internal.runtime.ScriptEnvironment;
 import jdk.nashorn.internal.runtime.ScriptRuntime;
 import jdk.nashorn.tools.Shell;
@@ -107,8 +108,29 @@
             }
 
             global.addShellBuiltins();
-            // expose history object for reflecting on command line history
-            global.put("history", new HistoryObject(in.getHistory()), false);
+
+            if (System.getSecurityManager() == null) {
+                // expose history object for reflecting on command line history
+                global.addOwnProperty("history", Property.NOT_ENUMERABLE, new HistoryObject(in.getHistory()));
+
+                // 'edit' command
+                global.addOwnProperty("edit", Property.NOT_ENUMERABLE, new EditObject(err::println,
+                    str -> {
+                        // could be called from different thread (GUI), we need to handle Context set/reset
+                        final Global _oldGlobal = Context.getGlobal();
+                        final boolean _globalChanged = (oldGlobal != global);
+                        if (_globalChanged) {
+                            Context.setGlobal(global);
+                        }
+                        try {
+                            evalImpl(context, global, str, err, env._dump_on_error);
+                        } finally {
+                            if (_globalChanged) {
+                                Context.setGlobal(_oldGlobal);
+                            }
+                        }
+                    }, in));
+            }
 
             while (true) {
                 String source = "";
@@ -128,17 +150,7 @@
                     continue;
                 }
 
-                try {
-                    final Object res = context.eval(global, source, global, "<shell>");
-                    if (res != ScriptRuntime.UNDEFINED) {
-                        err.println(JSType.toString(res));
-                    }
-                } catch (final Exception e) {
-                    err.println(e);
-                    if (env._dump_on_error) {
-                        e.printStackTrace(err);
-                    }
-                }
+                evalImpl(context, global, source, err, env._dump_on_error);
             }
         } catch (final Exception e) {
             err.println(e);
@@ -153,4 +165,19 @@
 
         return SUCCESS;
     }
+
+    private void evalImpl(final Context context, final Global global, final String source,
+            final PrintWriter err, final boolean doe) {
+        try {
+            final Object res = context.eval(global, source, global, "<shell>");
+            if (res != ScriptRuntime.UNDEFINED) {
+                err.println(JSType.toString(res));
+            }
+        } catch (final Exception e) {
+            err.println(e);
+            if (doe) {
+                e.printStackTrace(err);
+            }
+        }
+    }
 }