# HG changeset patch # User sundar # Date 1440160283 -19800 # Node ID 5beae9dfcdb9818780f96b851c72e3e2bb5801a1 # Parent 9b3eca69b88b2d1bebce92d58280ae66fc0b6091 8133948: Add 'edit' function to allow external editing of scripts Reviewed-by: attila, hannesw, jlahoda diff -r 9b3eca69b88b -r 5beae9dfcdb9 src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java --- a/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Console.java Thu Aug 20 12:29:58 2015 -0700 +++ b/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); + } + } } diff -r 9b3eca69b88b -r 5beae9dfcdb9 src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditObject.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/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 errorHandler; + private final Consumer evaluator; + private final Console console; + private String editor; + + EditObject(final Consumer errorHandler, final Consumer 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 { + 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; + } +} diff -r 9b3eca69b88b -r 5beae9dfcdb9 src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditPad.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/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 errorHandler; + private final String initialText; + private final boolean[] closeLock; + private final Consumer saveHandler; + + EditPad(Consumer errorHandler, String initialText, + boolean[] closeLock, Consumer 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 errorHandler, String initialText, + Consumer 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 + } + } + } + } +} diff -r 9b3eca69b88b -r 5beae9dfcdb9 src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/ExternalEditor.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/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 errorHandler; + private final Consumer saveHandler; + private final Console input; + + private WatchService watcher; + private Thread watchedThread; + private Path dir; + private Path tmpfile; + + ExternalEditor(Consumer errorHandler, Consumer 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 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 errorHandler, String initialText, + Consumer saveHandler, Console input) { + ExternalEditor ed = new ExternalEditor(errorHandler, saveHandler, input); + ed.edit(cmd, initialText); + } +} diff -r 9b3eca69b88b -r 5beae9dfcdb9 src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java --- a/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java Thu Aug 20 12:29:58 2015 -0700 +++ b/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, ""); - 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, ""); + if (res != ScriptRuntime.UNDEFINED) { + err.println(JSType.toString(res)); + } + } catch (final Exception e) { + err.println(e); + if (doe) { + e.printStackTrace(err); + } + } + } }