8133948: Add 'edit' function to allow external editing of scripts
Reviewed-by: attila, hannesw, jlahoda
--- 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);
+ }
+ }
+ }
}