8133948: Add 'edit' function to allow external editing of scripts

Reviewed-by: attila, hannesw, jlahoda
This commit is contained in:
Athijegannathan Sundararajan 2015-08-21 18:01:23 +05:30
parent 11dee9e7fe
commit 321ce034fc
5 changed files with 504 additions and 13 deletions

View File

@ -34,6 +34,11 @@ import java.util.Arrays;
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 @@ class Console implements AutoCloseable {
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 @@ class Console implements AutoCloseable {
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);
}
}
}

View File

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

View File

@ -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
}
}
}
}
}

View File

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

View File

@ -38,6 +38,7 @@ import jdk.nashorn.api.scripting.NashornException;
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 @@ public final class Main extends Shell {
}
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 @@ public final class Main extends Shell {
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 @@ public final class Main extends Shell {
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);
}
}
}
}