8133948: Add 'edit' function to allow external editing of scripts
Reviewed-by: attila, hannesw, jlahoda
This commit is contained in:
parent
11dee9e7fe
commit
321ce034fc
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user