/*
* Copyright (c) 2014, 2020, 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.internal.jshell.tool;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.internal.jshell.debug.InternalDebugControl;
import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
import jdk.jshell.DeclarationSnippet;
import jdk.jshell.Diag;
import jdk.jshell.EvalException;
import jdk.jshell.ExpressionSnippet;
import jdk.jshell.ImportSnippet;
import jdk.jshell.JShell;
import jdk.jshell.JShell.Subscription;
import jdk.jshell.JShellException;
import jdk.jshell.MethodSnippet;
import jdk.jshell.Snippet;
import jdk.jshell.Snippet.Kind;
import jdk.jshell.Snippet.Status;
import jdk.jshell.SnippetEvent;
import jdk.jshell.SourceCodeAnalysis;
import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
import jdk.jshell.SourceCodeAnalysis.Completeness;
import jdk.jshell.SourceCodeAnalysis.Suggestion;
import jdk.jshell.TypeDeclSnippet;
import jdk.jshell.UnresolvedReferenceException;
import jdk.jshell.VarSnippet;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.util.AbstractMap.SimpleEntry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Spliterators;
import java.util.function.Function;
import java.util.function.Supplier;
import jdk.internal.joptsimple.*;
import jdk.internal.jshell.tool.Feedback.FormatAction;
import jdk.internal.jshell.tool.Feedback.FormatCase;
import jdk.internal.jshell.tool.Feedback.FormatErrors;
import jdk.internal.jshell.tool.Feedback.FormatResolve;
import jdk.internal.jshell.tool.Feedback.FormatUnresolved;
import jdk.internal.jshell.tool.Feedback.FormatWhen;
import jdk.internal.editor.spi.BuildInEditorProvider;
import jdk.internal.editor.external.ExternalEditor;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static jdk.jshell.Snippet.SubKind.TEMP_VAR_EXPRESSION_SUBKIND;
import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND;
import static java.util.stream.Collectors.toMap;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP;
import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER;
/**
* Command line REPL tool for Java using the JShell API.
* @author Robert Field
*/
public class JShellTool implements MessageHandler {
private static final Pattern LINEBREAK = Pattern.compile("\\R");
private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?");
private static final Pattern RERUN_ID = Pattern.compile("/" + ID.pattern());
private static final Pattern RERUN_PREVIOUS = Pattern.compile("/\\-\\d+( .*)?");
private static final Pattern SET_SUB = Pattern.compile("/?set .*");
static final String RECORD_SEPARATOR = "\u241E";
private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources";
private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version";
private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n";
final InputStream cmdin;
final PrintStream cmdout;
final PrintStream cmderr;
final PrintStream console;
final InputStream userin;
final PrintStream userout;
final PrintStream usererr;
final PersistentStorage prefs;
final Map<String, String> envvars;
final Locale locale;
final Feedback feedback = new Feedback();
/**
* The complete constructor for the tool (used by test harnesses).
* @param cmdin command line input -- snippets and commands
* @param cmdout command line output, feedback including errors
* @param cmderr start-up errors and debugging info
* @param console console control interaction
* @param userin code execution input, or null to use IOContext
* @param userout code execution output -- System.out.printf("hi")
* @param usererr code execution error stream -- System.err.printf("Oops")
* @param prefs persistence implementation to use
* @param envvars environment variable mapping to use
* @param locale locale to use
*/
JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
PrintStream console,
InputStream userin, PrintStream userout, PrintStream usererr,
PersistentStorage prefs, Map<String, String> envvars, Locale locale) {
this.cmdin = cmdin;
this.cmdout = cmdout;
this.cmderr = cmderr;
this.console = console;
this.userin = userin != null ? userin : new InputStream() {
@Override
public int read() throws IOException {
return input.readUserInput();
}
};
this.userout = userout;
this.usererr = usererr;
this.prefs = prefs;
this.envvars = envvars;
this.locale = locale;
}
private ResourceBundle versionRB = null;
private ResourceBundle outputRB = null;
private IOContext input = null;
private boolean regenerateOnDeath = true;
private boolean live = false;
private boolean interactiveModeBegun = false;
private Options options;
SourceCodeAnalysis analysis;
private JShell state = null;
Subscription shutdownSubscription = null;
static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false);
private boolean debug = false;
private int debugFlags = 0;
public boolean testPrompt = false;
private Startup startup = null;
private boolean isCurrentlyRunningStartup = false;
private String executionControlSpec = null;
private EditorSetting editor = BUILT_IN_EDITOR;
private int exitCode = 0;
private static final String[] EDITOR_ENV_VARS = new String[] {
"JSHELLEDITOR", "VISUAL", "EDITOR"};
// Commands and snippets which can be replayed
private ReplayableHistory replayableHistory;
private ReplayableHistory replayableHistoryPrevious;
static final String STARTUP_KEY = "STARTUP";
static final String EDITOR_KEY = "EDITOR";
static final String FEEDBACK_KEY = "FEEDBACK";
static final String MODE_KEY = "MODE";
static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE";
static final Pattern BUILTIN_FILE_PATTERN = Pattern.compile("\\w+");
static final String BUILTIN_FILE_PATH_FORMAT = "/jdk/jshell/tool/resources/%s.jsh";
static final String INT_PREFIX = "int $$exit$$ = ";
static final int OUTPUT_WIDTH = 72;
// match anything followed by whitespace
private static final Pattern OPTION_PRE_PATTERN =
Pattern.compile("\\s*(\\S+\\s+)*?");
// match a (possibly incomplete) option flag with optional double-dash and/or internal dashes
private static final Pattern OPTION_PATTERN =
Pattern.compile(OPTION_PRE_PATTERN.pattern() + "(?<dd>-??)(?<flag>-([a-z][a-z\\-]*)?)");
// match an option flag and a (possibly missing or incomplete) value
private static final Pattern OPTION_VALUE_PATTERN =
Pattern.compile(OPTION_PATTERN.pattern() + "\\s+(?<val>\\S*)");
// Tool id (tid) mapping: the three name spaces
NameSpace mainNamespace;
NameSpace startNamespace;
NameSpace errorNamespace;
// Tool id (tid) mapping: the current name spaces
NameSpace currentNameSpace;
Map<Snippet, SnippetInfo> mapSnippet;
// Kinds of compiler/runtime init options
private enum OptionKind {
CLASS_PATH("--class-path", true),
MODULE_PATH("--module-path", true),
ADD_MODULES("--add-modules", false),
ADD_EXPORTS("--add-exports", false),
ENABLE_PREVIEW("--enable-preview", true),
SOURCE_RELEASE("-source", true, true, true, false, false), // virtual option, generated by --enable-preview
TO_COMPILER("-C", false, false, true, false, false),
TO_REMOTE_VM("-R", false, false, false, true, false),;
final String optionFlag;
final boolean onlyOne;
final boolean passFlag;
final boolean toCompiler;
final boolean toRemoteVm;
final boolean showOption;
private OptionKind(String optionFlag, boolean onlyOne) {
this(optionFlag, onlyOne, true, true, true, true);
}
private OptionKind(String optionFlag, boolean onlyOne, boolean passFlag, boolean toCompiler, boolean toRemoteVm, boolean showOption) {
this.optionFlag = optionFlag;
this.onlyOne = onlyOne;
this.passFlag = passFlag;
this.toCompiler = toCompiler;
this.toRemoteVm = toRemoteVm;
this.showOption= showOption;
}
}
// compiler/runtime init option values
private static class Options {
private final Map<OptionKind, List<String>> optMap;
// New blank Options
Options() {
optMap = new HashMap<>();
}
// Options as a copy
private Options(Options opts) {
optMap = new HashMap<>(opts.optMap);
}
private String[] selectOptions(Predicate<Entry<OptionKind, List<String>>> pred) {
return optMap.entrySet().stream()
.filter(pred)
.flatMap(e -> e.getValue().stream())
.toArray(String[]::new);
}
String[] remoteVmOptions() {
return selectOptions(e -> e.getKey().toRemoteVm);
}
String[] compilerOptions() {
return selectOptions(e -> e.getKey().toCompiler);
}
String[] shownOptions() {
return selectOptions(e -> e.getKey().showOption);
}
void addAll(OptionKind kind, Collection<String> vals) {
optMap.computeIfAbsent(kind, k -> new ArrayList<>())
.addAll(vals);
}
// return a new Options, with parameter options overriding receiver options
Options override(Options newer) {
Options result = new Options(this);
newer.optMap.entrySet().stream()
.forEach(e -> {
if (e.getKey().onlyOne) {
// Only one allowed, override last
result.optMap.put(e.getKey(), e.getValue());
} else {
// Additive
result.addAll(e.getKey(), e.getValue());
}
});
return result;
}
}
// base option parsing of /env, /reload, and /reset and command-line options
private class OptionParserBase {
final OptionParser parser = new OptionParser();
private final OptionSpec<String> argClassPath = parser.accepts("class-path").withRequiredArg();
private final OptionSpec<String> argModulePath = parser.accepts("module-path").withRequiredArg();
private final OptionSpec<String> argAddModules = parser.accepts("add-modules").withRequiredArg();
private final OptionSpec<String> argAddExports = parser.accepts("add-exports").withRequiredArg();
private final OptionSpecBuilder argEnablePreview = parser.accepts("enable-preview");
private final NonOptionArgumentSpec<String> argNonOptions = parser.nonOptions();
private Options opts = new Options();
private List<String> nonOptions;
private boolean failed = false;
List<String> nonOptions() {
return nonOptions;
}
void msg(String key, Object... args) {
errormsg(key, args);
}
Options parse(String[] args) throws OptionException {
try {
OptionSet oset = parser.parse(args);
nonOptions = oset.valuesOf(argNonOptions);
return parse(oset);
} catch (OptionException ex) {
if (ex.options().isEmpty()) {
msg("jshell.err.opt.invalid", stream(args).collect(joining(", ")));
} else {
boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next());
msg(isKnown
? "jshell.err.opt.arg"
: "jshell.err.opt.unknown",
ex.options()
.stream()
.collect(joining(", ")));
}
exitCode = 1;
return null;
}
}
// check that the supplied string represent valid class/module paths
// converting any ~/ to user home
private Collection<String> validPaths(Collection<String> vals, String context, boolean isModulePath) {
Stream<String> result = vals.stream()
.map(s -> Arrays.stream(s.split(File.pathSeparator))
.flatMap(sp -> toPathImpl(sp, context))
.filter(p -> checkValidPathEntry(p, context, isModulePath))
.map(p -> p.toString())
.collect(Collectors.joining(File.pathSeparator)));
if (failed) {
return Collections.emptyList();
} else {
return result.collect(toList());
}
}
// Adapted from compiler method Locations.checkValidModulePathEntry
private boolean checkValidPathEntry(Path p, String context, boolean isModulePath) {
if (!Files.exists(p)) {
msg("jshell.err.file.not.found", context, p);
failed = true;
return false;
}
if (Files.isDirectory(p)) {
// if module-path, either an exploded module or a directory of modules
return true;
}
String name = p.getFileName().toString();
int lastDot = name.lastIndexOf(".");
if (lastDot > 0) {
switch (name.substring(lastDot)) {
case ".jar":
return true;
case ".jmod":
if (isModulePath) {
return true;
}
}
}
msg("jshell.err.arg", context, p);
failed = true;
return false;
}
private Stream<Path> toPathImpl(String path, String context) {
try {
return Stream.of(toPathResolvingUserHome(path));
} catch (InvalidPathException ex) {
msg("jshell.err.file.not.found", context, path);
failed = true;
return Stream.empty();
}
}
Options parse(OptionSet options) {
addOptions(OptionKind.CLASS_PATH,
validPaths(options.valuesOf(argClassPath), "--class-path", false));
addOptions(OptionKind.MODULE_PATH,
validPaths(options.valuesOf(argModulePath), "--module-path", true));
addOptions(OptionKind.ADD_MODULES, options.valuesOf(argAddModules));
addOptions(OptionKind.ADD_EXPORTS, options.valuesOf(argAddExports).stream()
.map(mp -> mp.contains("=") ? mp : mp + "=ALL-UNNAMED")
.collect(toList())
);
if (options.has(argEnablePreview)) {
opts.addAll(OptionKind.ENABLE_PREVIEW, List.of(
OptionKind.ENABLE_PREVIEW.optionFlag));
opts.addAll(OptionKind.SOURCE_RELEASE, List.of(
OptionKind.SOURCE_RELEASE.optionFlag,
System.getProperty("java.specification.version")));
}
if (failed) {
exitCode = 1;
return null;
} else {
return opts;
}
}
void addOptions(OptionKind kind, Collection<String> vals) {
if (!vals.isEmpty()) {
if (kind.onlyOne && vals.size() > 1) {
msg("jshell.err.opt.one", kind.optionFlag);
failed = true;
return;
}
if (kind.passFlag) {
vals = vals.stream()
.flatMap(mp -> Stream.of(kind.optionFlag, mp))
.collect(toList());
}
opts.addAll(kind, vals);
}
}
}
// option parsing for /reload (adds -restore -quiet)
private class OptionParserReload extends OptionParserBase {
private final OptionSpecBuilder argRestore = parser.accepts("restore");
private final OptionSpecBuilder argQuiet = parser.accepts("quiet");
private boolean restore = false;
private boolean quiet = false;
boolean restore() {
return restore;
}
boolean quiet() {
return quiet;
}
@Override
Options parse(OptionSet options) {
if (options.has(argRestore)) {
restore = true;
}
if (options.has(argQuiet)) {
quiet = true;
}
return super.parse(options);
}
}
// option parsing for command-line
private class OptionParserCommandLine extends OptionParserBase {
private final OptionSpec<String> argStart = parser.accepts("startup").withRequiredArg();
private final OptionSpecBuilder argNoStart = parser.acceptsAll(asList("n", "no-startup"));
private final OptionSpec<String> argFeedback = parser.accepts("feedback").withRequiredArg();
private final OptionSpec<String> argExecution = parser.accepts("execution").withRequiredArg();
private final OptionSpecBuilder argQ = parser.accepts("q");
private final OptionSpecBuilder argS = parser.accepts("s");
private final OptionSpecBuilder argV = parser.accepts("v");
private final OptionSpec<String> argR = parser.accepts("R").withRequiredArg();
private final OptionSpec<String> argC = parser.accepts("C").withRequiredArg();
private final OptionSpecBuilder argHelp = parser.acceptsAll(asList("?", "h", "help"));
private final OptionSpecBuilder argVersion = parser.accepts("version");
private final OptionSpecBuilder argFullVersion = parser.accepts("full-version");
private final OptionSpecBuilder argShowVersion = parser.accepts("show-version");
private final OptionSpecBuilder argHelpExtra = parser.acceptsAll(asList("X", "help-extra"));
private String feedbackMode = null;
private Startup initialStartup = null;
String feedbackMode() {
return feedbackMode;
}
Startup startup() {
return initialStartup;
}
@Override
void msg(String key, Object... args) {
errormsg(key, args);
}
/**
* Parse the command line options.
* @return the options as an Options object, or null if error
*/
@Override
Options parse(OptionSet options) {
if (options.has(argHelp)) {
printUsage();
return null;
}
if (options.has(argHelpExtra)) {
printUsageX();
return null;
}
if (options.has(argVersion)) {
cmdout.printf("jshell %s\n", version());
return null;
}
if (options.has(argFullVersion)) {
cmdout.printf("jshell %s\n", fullVersion());
return null;
}
if (options.has(argShowVersion)) {
cmdout.printf("jshell %s\n", version());
}
if ((options.valuesOf(argFeedback).size() +
(options.has(argQ) ? 1 : 0) +
(options.has(argS) ? 1 : 0) +
(options.has(argV) ? 1 : 0)) > 1) {
msg("jshell.err.opt.feedback.one");
exitCode = 1;
return null;
} else if (options.has(argFeedback)) {
feedbackMode = options.valueOf(argFeedback);
} else if (options.has("q")) {
feedbackMode = "concise";
} else if (options.has("s")) {
feedbackMode = "silent";
} else if (options.has("v")) {
feedbackMode = "verbose";
}
if (options.has(argStart)) {
List<String> sts = options.valuesOf(argStart);
if (options.has("no-startup")) {
msg("jshell.err.opt.startup.conflict");
exitCode = 1;
return null;
}
initialStartup = Startup.fromFileList(sts, "--startup", new InitMessageHandler());
if (initialStartup == null) {
exitCode = 1;
return null;
}
} else if (options.has(argNoStart)) {
initialStartup = Startup.noStartup();
} else {
String packedStartup = prefs.get(STARTUP_KEY);
initialStartup = Startup.unpack(packedStartup, new InitMessageHandler());
}
if (options.has(argExecution)) {
executionControlSpec = options.valueOf(argExecution);
}
addOptions(OptionKind.TO_REMOTE_VM, options.valuesOf(argR));
addOptions(OptionKind.TO_COMPILER, options.valuesOf(argC));
return super.parse(options);
}
}
/**
* Encapsulate a history of snippets and commands which can be replayed.
*/
private static class ReplayableHistory {
// the history
private List<String> hist;
// the length of the history as of last save
private int lastSaved;
private ReplayableHistory(List<String> hist) {
this.hist = hist;
this.lastSaved = 0;
}
// factory for empty histories
static ReplayableHistory emptyHistory() {
return new ReplayableHistory(new ArrayList<>());
}
// factory for history stored in persistent storage
static ReplayableHistory fromPrevious(PersistentStorage prefs) {
// Read replay history from last jshell session
String prevReplay = prefs.get(REPLAY_RESTORE_KEY);
if (prevReplay == null) {
return null;
} else {
return new ReplayableHistory(Arrays.asList(prevReplay.split(RECORD_SEPARATOR)));
}
}
// store the history in persistent storage
void storeHistory(PersistentStorage prefs) {
if (hist.size() > lastSaved) {
// Prevent history overflow by calculating what will fit, starting
// with most recent
int sepLen = RECORD_SEPARATOR.length();
int length = 0;
int first = hist.size();
while (length < Preferences.MAX_VALUE_LENGTH && --first >= 0) {
length += hist.get(first).length() + sepLen;
}
if (first >= 0) {
hist = hist.subList(first + 1, hist.size());
}
String shist = String.join(RECORD_SEPARATOR, hist);
prefs.put(REPLAY_RESTORE_KEY, shist);
markSaved();
}
prefs.flush();
}
// add a snippet or command to the history
void add(String s) {
hist.add(s);
}
// return history to reloaded
Iterable<String> iterable() {
return hist;
}
// mark that persistent storage and current history are in sync
void markSaved() {
lastSaved = hist.size();
}
}
/**
* Is the input/output currently interactive
*
* @return true if console
*/
boolean interactive() {
return input != null && input.interactiveOutput();
}
void debug(String format, Object... args) {
if (debug) {
cmderr.printf(format + "\n", args);
}
}
/**
* Must show command output
*
* @param format printf format
* @param args printf args
*/
@Override
public void hard(String format, Object... args) {
cmdout.printf(prefix(format), args);
}
/**
* Error command output
*
* @param format printf format
* @param args printf args
*/
void error(String format, Object... args) {
(interactiveModeBegun? cmdout : cmderr).printf(prefixError(format), args);
}
/**
* Should optional informative be displayed?
* @return true if they should be displayed
*/
@Override
public boolean showFluff() {
return feedback.shouldDisplayCommandFluff() && interactive();
}
/**
* Optional output
*
* @param format printf format
* @param args printf args
*/
@Override
public void fluff(String format, Object... args) {
if (showFluff()) {
hard(format, args);
}
}
/**
* Resource bundle look-up
*
* @param key the resource key
*/
String getResourceString(String key) {
if (outputRB == null) {
try {
outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale);
} catch (MissingResourceException mre) {
error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale);
return "";
}
}
String s;
try {
s = outputRB.getString(key);
} catch (MissingResourceException mre) {
error("Missing resource: %s in %s", key, L10N_RB_NAME);
return "";
}
return s;
}
/**
* Add normal prefixing/postfixing to embedded newlines in a string,
* bracketing with normal prefix/postfix
*
* @param s the string to prefix
* @return the pre/post-fixed and bracketed string
*/
String prefix(String s) {
return prefix(s, feedback.getPre(), feedback.getPost());
}
/**
* Add error prefixing/postfixing to embedded newlines in a string,
* bracketing with error prefix/postfix
*
* @param s the string to prefix
* @return the pre/post-fixed and bracketed string
*/
String prefixError(String s) {
return prefix(s, feedback.getErrorPre(), feedback.getErrorPost());
}
/**
* Add prefixing/postfixing to embedded newlines in a string,
* bracketing with prefix/postfix. No prefixing when non-interactive.
* Result is expected to be the format for a printf.
*
* @param s the string to prefix
* @param pre the string to prepend to each line
* @param post the string to append to each line (replacing newline)
* @return the pre/post-fixed and bracketed string
*/
String prefix(String s, String pre, String post) {
if (s == null) {
return "";
}
if (!interactiveModeBegun) {
// messages expect to be new-line terminated (even when not prefixed)
return s + "%n";
}
String pp = s.replaceAll("\\R", post + pre);
if (pp.endsWith(post + pre)) {
// prevent an extra prefix char and blank line when the string
// already terminates with newline
pp = pp.substring(0, pp.length() - (post + pre).length());
}
return pre + pp + post;
}
/**
* Print using resource bundle look-up and adding prefix and postfix
*
* @param key the resource key
*/
void hardrb(String key) {
hard(getResourceString(key));
}
/**
* Format using resource bundle look-up using MessageFormat
*
* @param key the resource key
* @param args
*/
String messageFormat(String key, Object... args) {
String rs = getResourceString(key);
return MessageFormat.format(rs, args);
}
/**
* Print using resource bundle look-up, MessageFormat, and add prefix and
* postfix
*
* @param key the resource key
* @param args
*/
@Override
public void hardmsg(String key, Object... args) {
hard(messageFormat(key, args));
}
/**
* Print error using resource bundle look-up, MessageFormat, and add prefix
* and postfix
*
* @param key the resource key
* @param args
*/
@Override
public void errormsg(String key, Object... args) {
error("%s", messageFormat(key, args));
}
/**
* Print (fluff) using resource bundle look-up, MessageFormat, and add
* prefix and postfix
*
* @param key the resource key
* @param args
*/
@Override
public void fluffmsg(String key, Object... args) {
if (showFluff()) {
hardmsg(key, args);
}
}
<T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
Map<String, String> a2b = stream.collect(toMap(a, b,
(m1, m2) -> m1,
LinkedHashMap::new));
for (Entry<String, String> e : a2b.entrySet()) {
hard("%s", e.getKey());
cmdout.printf(prefix(e.getValue(), feedback.getPre() + "\t", feedback.getPost()));
}
}
/**
* Trim whitespace off end of string
*
* @param s
* @return
*/
static String trimEnd(String s) {
int last = s.length() - 1;
int i = last;
while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
--i;
}
if (i != last) {
return s.substring(0, i + 1);
} else {
return s;
}
}
/**
* The entry point into the JShell tool.
*
* @param args the command-line arguments
* @throws Exception catastrophic fatal exception
* @return the exit code
*/
public int start(String[] args) throws Exception {
OptionParserCommandLine commandLineArgs = new OptionParserCommandLine();
options = commandLineArgs.parse(args);
if (options == null) {
// A null means end immediately, this may be an error or because
// of options like --version. Exit code has been set.
return exitCode;
}
startup = commandLineArgs.startup();
// initialize editor settings
configEditor();
// initialize JShell instance
try {
resetState();
} catch (IllegalStateException ex) {
// Display just the cause (not a exception backtrace)
cmderr.println(ex.getMessage());
//abort
return 1;
}
// Read replay history from last jshell session into previous history
replayableHistoryPrevious = ReplayableHistory.fromPrevious(prefs);
// load snippet/command files given on command-line
for (String loadFile : commandLineArgs.nonOptions()) {
if (!runFile(loadFile, "jshell")) {
// Load file failed -- abort
return 1;
}
}
// if we survived that...
if (regenerateOnDeath) {
// initialize the predefined feedback modes
initFeedback(commandLineArgs.feedbackMode());
}
// check again, as feedback setting could have failed
if (regenerateOnDeath) {
// if we haven't died, and the feedback mode wants fluff, print welcome
interactiveModeBegun = true;
if (feedback.shouldDisplayCommandFluff()) {
hardmsg("jshell.msg.welcome", version());
}
// Be sure history is always saved so that user code isn't lost
Thread shutdownHook = new Thread() {
@Override
public void run() {
replayableHistory.storeHistory(prefs);
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
// execute from user input
try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
while (regenerateOnDeath) {
if (!live) {
resetState();
}
run(in);
}
} finally {
replayableHistory.storeHistory(prefs);
closeState();
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (Exception ex) {
// ignore, this probably caused by VM aready being shutdown
// and this is the last act anyhow
}
}
}
closeState();
return exitCode;
}
private EditorSetting configEditor() {
// Read retained editor setting (if any)
editor = EditorSetting.fromPrefs(prefs);
if (editor != null) {
return editor;
}
// Try getting editor setting from OS environment variables
for (String envvar : EDITOR_ENV_VARS) {
String v = envvars.get(envvar);
if (v != null) {
return editor = new EditorSetting(v.split("\\s+"), false);
}
}
// Default to the built-in editor
return editor = BUILT_IN_EDITOR;
}
private void printUsage() {
cmdout.print(getResourceString("help.usage"));
}
private void printUsageX() {
cmdout.print(getResourceString("help.usage.x"));
}
/**
* Message handler to use during initial start-up.
*/
private class InitMessageHandler implements MessageHandler {
@Override
public void fluff(String format, Object... args) {
//ignore
}
@Override
public void fluffmsg(String messageKey, Object... args) {
//ignore
}
@Override
public void hard(String format, Object... args) {
//ignore
}
@Override
public void hardmsg(String messageKey, Object... args) {
//ignore
}
@Override
public void errormsg(String messageKey, Object... args) {
JShellTool.this.errormsg(messageKey, args);
}
@Override
public boolean showFluff() {
return false;
}
}
private void resetState() {
closeState();
// Initialize tool id mapping
mainNamespace = new NameSpace("main", "");
startNamespace = new NameSpace("start", "s");
errorNamespace = new NameSpace("error", "e");
mapSnippet = new LinkedHashMap<>();
currentNameSpace = startNamespace;
// Reset the replayable history, saving the old for restore
replayableHistoryPrevious = replayableHistory;
replayableHistory = ReplayableHistory.emptyHistory();
JShell.Builder builder =
JShell.builder()
.in(userin)
.out(userout)
.err(usererr)
.tempVariableNameGenerator(() -> "$" + currentNameSpace.tidNext())
.idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive())
? currentNameSpace.tid(sn)
: errorNamespace.tid(sn))
.remoteVMOptions(options.remoteVmOptions())
.compilerOptions(options.compilerOptions());
if (executionControlSpec != null) {
builder.executionEngine(executionControlSpec);
}
state = builder.build();
InternalDebugControl.setDebugFlags(state, debugFlags);
shutdownSubscription = state.onShutdown((JShell deadState) -> {
if (deadState == state) {
hardmsg("jshell.msg.terminated");
fluffmsg("jshell.msg.terminated.restore");
live = false;
}
});
analysis = state.sourceCodeAnalysis();
live = true;
// Run the start-up script.
// Avoid an infinite loop running start-up while running start-up.
// This could, otherwise, occur when /env /reset or /reload commands are
// in the start-up script.
if (!isCurrentlyRunningStartup) {
try {
isCurrentlyRunningStartup = true;
startUpRun(startup.toString());
} finally {
isCurrentlyRunningStartup = false;
}
}
// Record subsequent snippets in the main namespace.
currentNameSpace = mainNamespace;
}
//where -- one-time per run initialization of feedback modes
private void initFeedback(String initMode) {
// No fluff, no prefix, for init failures
MessageHandler initmh = new InitMessageHandler();
// Execute the feedback initialization code in the resource file
startUpRun(getResourceString("startup.feedback"));
// These predefined modes are read-only
feedback.markModesReadOnly();
// Restore user defined modes retained on previous run with /set mode -retain
String encoded = prefs.get(MODE_KEY);
if (encoded != null && !encoded.isEmpty()) {
if (!feedback.restoreEncodedModes(initmh, encoded)) {
// Catastrophic corruption -- remove the retained modes
prefs.remove(MODE_KEY);
}
}
if (initMode != null) {
// The feedback mode to use was specified on the command line, use it
if (!setFeedback(initmh, new ArgTokenizer("--feedback", initMode))) {
regenerateOnDeath = false;
exitCode = 1;
}
} else {
String fb = prefs.get(FEEDBACK_KEY);
if (fb != null) {
// Restore the feedback mode to use that was retained
// on a previous run with /set feedback -retain
setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb));
}
}
}
//where
private void startUpRun(String start) {
try (IOContext suin = new ScannerIOContext(new StringReader(start))) {
run(suin);
} catch (Exception ex) {
errormsg("jshell.err.startup.unexpected.exception", ex);
ex.printStackTrace(cmderr);
}
}
private void closeState() {
live = false;
JShell oldState = state;
if (oldState != null) {
state = null;
analysis = null;
oldState.unsubscribe(shutdownSubscription); // No notification
oldState.close();
}
}
/**
* Main loop
*
* @param in the line input/editing context
*/
private void run(IOContext in) {
IOContext oldInput = input;
input = in;
try {
// remaining is the source left after one snippet is evaluated
String remaining = "";
while (live) {
// Get a line(s) of input
String src = getInput(remaining);
// Process the snippet or command, returning the remaining source
remaining = processInput(src);
}
} catch (EOFException ex) {
// Just exit loop
} catch (IOException ex) {
errormsg("jshell.err.unexpected.exception", ex);
} finally {
input = oldInput;
}
}
/**
* Process an input command or snippet.
*
* @param src the source to process
* @return any remaining input to processed
*/
private String processInput(String src) {
if (isCommand(src)) {
// It is a command
processCommand(src.trim());
// No remaining input after a command
return "";
} else {
// It is a snipet. Separate the source from the remaining. Evaluate
// the source
CompletionInfo an = analysis.analyzeCompletion(src);
if (processSourceCatchingReset(trimEnd(an.source()))) {
// Snippet was successful use any leftover source
return an.remaining();
} else {
// Snippet failed, throw away any remaining source
return "";
}
}
}
/**
* Get the input line (or, if incomplete, lines).
*
* @param initial leading input (left over after last snippet)
* @return the complete input snippet or command
* @throws IOException on unexpected I/O error
*/
private String getInput(String initial) throws IOException{
String src = initial;
while (live) { // loop while incomplete (and live)
if (!src.isEmpty() && isComplete(src)) {
return src;
}
String firstLinePrompt = interactive()
? testPrompt ? " \005"
: feedback.getPrompt(currentNameSpace.tidNext())
: "" // Non-interactive -- no prompt
;
String continuationPrompt = interactive()
? testPrompt ? " \006"
: feedback.getContinuationPrompt(currentNameSpace.tidNext())
: "" // Non-interactive -- no prompt
;
String line;
try {
line = input.readLine(firstLinePrompt, continuationPrompt, src.isEmpty(), src);
} catch (InputInterruptedException ex) {
//input interrupted - clearing current state
src = "";
continue;
}
if (line == null) {
//EOF
if (input.interactiveOutput()) {
// End after user ctrl-D
regenerateOnDeath = false;
}
throw new EOFException(); // no more input
}
src = src.isEmpty()
? line
: src + "\n" + line;
}
throw new EOFException(); // not longer live
}
public boolean isComplete(String src) {
String check;
if (isCommand(src)) {
// A command can only be incomplete if it is a /exit with
// an argument
int sp = src.indexOf(" ");
if (sp < 0) return true;
check = src.substring(sp).trim();
if (check.isEmpty()) return true;
String cmd = src.substring(0, sp);
Command[] match = findCommand(cmd, c -> c.kind.isRealCommand);
if (match.length != 1 || !match[0].command.equals("/exit")) {
// A command with no snippet arg, so no multi-line input
return true;
}
} else {
// For a snippet check the whole source
check = src;
}
Completeness comp = analysis.analyzeCompletion(check).completeness();
if (comp.isComplete() || comp == Completeness.EMPTY) {
return true;
}
return false;
}
private boolean isCommand(String line) {
return line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*");
}
private void addToReplayHistory(String s) {
if (!isCurrentlyRunningStartup) {
replayableHistory.add(s);
}
}
/**
* Process a source snippet.
*
* @param src the snippet source to process
* @return true on success, false on failure
*/
private boolean processSourceCatchingReset(String src) {
try {
input.beforeUserCode();
return processSource(src);
} catch (IllegalStateException ex) {
hard("Resetting...");
live = false; // Make double sure
return false;
} finally {
input.afterUserCode();
}
}
/**
* Process a command (as opposed to a snippet) -- things that start with
* slash.
*
* @param input
*/
private void processCommand(String input) {
if (input.startsWith("/-")) {
try {
//handle "/-[number]"
cmdUseHistoryEntry(Integer.parseInt(input.substring(1)));
return ;
} catch (NumberFormatException ex) {
//ignore
}
}
String cmd;
String arg;
int idx = input.indexOf(' ');
if (idx > 0) {
arg = input.substring(idx + 1).trim();
cmd = input.substring(0, idx);
} else {
cmd = input;
arg = "";
}
// find the command as a "real command", not a pseudo-command or doc subject
Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand);
switch (candidates.length) {
case 0:
// not found, it is either a rerun-ID command or an error
if (RERUN_ID.matcher(cmd).matches()) {
// it is in the form of a snipppet id, see if it is a valid history reference
rerunHistoryEntriesById(input);
} else {
errormsg("jshell.err.invalid.command", cmd);
fluffmsg("jshell.msg.help.for.help");
}
break;
case 1:
Command command = candidates[0];
// If comand was successful and is of a replayable kind, add it the replayable history
if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) {
addToReplayHistory((command.command + " " + arg).trim());
}
break;
default:
// command if too short (ambigous), show the possibly matches
errormsg("jshell.err.command.ambiguous", cmd,
Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
fluffmsg("jshell.msg.help.for.help");
break;
}
}
private Command[] findCommand(String cmd, Predicate<Command> filter) {
Command exact = commands.get(cmd);
if (exact != null)
return new Command[] {exact};
return commands.values()
.stream()
.filter(filter)
.filter(command -> command.command.startsWith(cmd))
.toArray(Command[]::new);
}
static Path toPathResolvingUserHome(String pathString) {
if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
return Paths.get(System.getProperty("user.home"), pathString.substring(2));
else
return Paths.get(pathString);
}
static final class Command {
public final String command;
public final String helpKey;
public final Function<String,Boolean> run;
public final CompletionProvider completions;
public final CommandKind kind;
// NORMAL Commands
public Command(String command, Function<String,Boolean> run, CompletionProvider completions) {
this(command, run, completions, CommandKind.NORMAL);
}
// Special kinds of Commands
public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
this(command, "help." + command.substring(1),
run, completions, kind);
}
// Documentation pseudo-commands
public Command(String command, String helpKey, CommandKind kind) {
this(command, helpKey,
arg -> { throw new IllegalStateException(); },
EMPTY_COMPLETION_PROVIDER,
kind);
}
public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
this.command = command;
this.helpKey = helpKey;
this.run = run;
this.completions = completions;
this.kind = kind;
}
}
interface CompletionProvider {
List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
}
enum CommandKind {
NORMAL(true, true, true),
REPLAY(true, true, true),
HIDDEN(true, false, false),
HELP_ONLY(false, true, false),
HELP_SUBJECT(false, false, false);
final boolean isRealCommand;
final boolean showInHelp;
final boolean shouldSuggestCompletions;
private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) {
this.isRealCommand = isRealCommand;
this.showInHelp = showInHelp;
this.shouldSuggestCompletions = shouldSuggestCompletions;
}
}
static final class FixedCompletionProvider implements CompletionProvider {
private final String[] alternatives;
public FixedCompletionProvider(String... alternatives) {
this.alternatives = alternatives;
}
// Add more options to an existing provider
public FixedCompletionProvider(FixedCompletionProvider base, String... alternatives) {
List<String> l = new ArrayList<>(Arrays.asList(base.alternatives));
l.addAll(Arrays.asList(alternatives));
this.alternatives = l.toArray(new String[l.size()]);
}
@Override
public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
List<Suggestion> result = new ArrayList<>();
for (String alternative : alternatives) {
if (alternative.startsWith(input)) {
result.add(new ArgSuggestion(alternative));
}
}
anchor[0] = 0;
return result;
}
}
static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
private static final CompletionProvider SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start ", "-history");
private static final CompletionProvider SAVE_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history ");
private static final CompletionProvider HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all");
private static final CompletionProvider SNIPPET_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start " );
private static final FixedCompletionProvider COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
"-class-path ", "-module-path ", "-add-modules ", "-add-exports ");
private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER,
"-restore ", "-quiet ");
private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete");
private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
private static final Map<String, CompletionProvider> ARG_OPTIONS = new HashMap<>();
static {
ARG_OPTIONS.put("-class-path", classPathCompletion());
ARG_OPTIONS.put("-module-path", fileCompletions(Files::isDirectory));
ARG_OPTIONS.put("-add-modules", EMPTY_COMPLETION_PROVIDER);
ARG_OPTIONS.put("-add-exports", EMPTY_COMPLETION_PROVIDER);
}
private final Map<String, Command> commands = new LinkedHashMap<>();
private void registerCommand(Command cmd) {
commands.put(cmd.command, cmd);
}
private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) {
return (input, cursor, anchor) -> {
List<Suggestion> result = Collections.emptyList();
int space = input.indexOf(' ');
if (space != -1) {
String rest = input.substring(space + 1);
result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor);
anchor[0] += space + 1;
}
return result;
};
}
private static CompletionProvider fileCompletions(Predicate<Path> accept) {
return (code, cursor, anchor) -> {
int lastSlash = code.lastIndexOf('/');
String path = code.substring(0, lastSlash + 1);
String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
Path current = toPathResolvingUserHome(path);
List<Suggestion> result = new ArrayList<>();
try (Stream<Path> dir = Files.list(current)) {
dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
.map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : "")))
.forEach(result::add);
} catch (IOException ex) {
//ignore...
}
if (path.isEmpty()) {
StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
.filter(root -> Files.exists(root))
.filter(root -> accept.test(root) && root.toString().startsWith(prefix))
.map(root -> new ArgSuggestion(root.toString()))
.forEach(result::add);
}
anchor[0] = path.length();
return result;
};
}
private static CompletionProvider classPathCompletion() {
return fileCompletions(p -> Files.isDirectory(p) ||
p.getFileName().toString().endsWith(".zip") ||
p.getFileName().toString().endsWith(".jar"));
}
// Completion based on snippet supplier
private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) {
return (prefix, cursor, anchor) -> {
anchor[0] = 0;
int space = prefix.lastIndexOf(' ');
Set<String> prior = new HashSet<>(Arrays.asList(prefix.split(" ")));
if (prior.contains("-all") || prior.contains("-history")) {
return Collections.emptyList();
}
String argPrefix = prefix.substring(space + 1);
return snippetsSupplier.get()
.filter(k -> !prior.contains(String.valueOf(k.id()))
&& (!(k instanceof DeclarationSnippet)
|| !prior.contains(((DeclarationSnippet) k).name())))
.flatMap(k -> (k instanceof DeclarationSnippet)
? Stream.of(String.valueOf(k.id()) + " ", ((DeclarationSnippet) k).name() + " ")
: Stream.of(String.valueOf(k.id()) + " "))
.filter(k -> k.startsWith(argPrefix))
.map(ArgSuggestion::new)
.collect(Collectors.toList());
};
}
// Completion based on snippet supplier with -all -start (and sometimes -history) options
private CompletionProvider snippetWithOptionCompletion(CompletionProvider optionProvider,
Supplier<Stream<? extends Snippet>> snippetsSupplier) {
return (code, cursor, anchor) -> {
List<Suggestion> result = new ArrayList<>();
int pastSpace = code.lastIndexOf(' ') + 1; // zero if no space
if (pastSpace == 0) {
result.addAll(optionProvider.completionSuggestions(code, cursor, anchor));
}
result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor));
anchor[0] += pastSpace;
return result;
};
}
// Completion of help, commands and subjects
private CompletionProvider helpCompletion() {
return (code, cursor, anchor) -> {
List<Suggestion> result;
int pastSpace = code.indexOf(' ') + 1; // zero if no space
if (pastSpace == 0) {
// initially suggest commands (with slash) and subjects,
// however, if their subject starts without slash, include
// commands without slash
boolean noslash = code.length() > 0 && !code.startsWith("/");
result = new FixedCompletionProvider(commands.values().stream()
.filter(cmd -> cmd.kind.showInHelp || cmd.kind == CommandKind.HELP_SUBJECT)
.map(c -> ((noslash && c.command.startsWith("/"))
? c.command.substring(1)
: c.command) + " ")
.toArray(String[]::new))
.completionSuggestions(code, cursor, anchor);
} else if (code.startsWith("/se") || code.startsWith("se")) {
result = new FixedCompletionProvider(SET_SUBCOMMANDS)
.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor);
} else {
result = Collections.emptyList();
}
anchor[0] += pastSpace;
return result;
};
}
private static CompletionProvider saveCompletion() {
return (code, cursor, anchor) -> {
List<Suggestion> result = new ArrayList<>();
int space = code.indexOf(' ');
if (space == (-1)) {
result.addAll(SAVE_OPTION_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
}
result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
anchor[0] += space + 1;
return result;
};
}
// command-line-like option completion -- options with values
private static CompletionProvider optionCompletion(CompletionProvider provider) {
return (code, cursor, anchor) -> {
Matcher ovm = OPTION_VALUE_PATTERN.matcher(code);
if (ovm.matches()) {
String flag = ovm.group("flag");
List<CompletionProvider> ps = ARG_OPTIONS.entrySet().stream()
.filter(es -> es.getKey().startsWith(flag))
.map(es -> es.getValue())
.collect(toList());
if (ps.size() == 1) {
int pastSpace = ovm.start("val");
List<Suggestion> result = ps.get(0).completionSuggestions(
ovm.group("val"), cursor - pastSpace, anchor);
anchor[0] += pastSpace;
return result;
}
}
Matcher om = OPTION_PATTERN.matcher(code);
if (om.matches()) {
int pastSpace = om.start("flag");
List<Suggestion> result = provider.completionSuggestions(
om.group("flag"), cursor - pastSpace, anchor);
if (!om.group("dd").isEmpty()) {
result = result.stream()
.map(sug -> new Suggestion() {
@Override
public String continuation() {
return "-" + sug.continuation();
}
@Override
public boolean matchesType() {
return false;
}
})
.collect(toList());
--pastSpace;
}
anchor[0] += pastSpace;
return result;
}
Matcher opp = OPTION_PRE_PATTERN.matcher(code);
if (opp.matches()) {
int pastSpace = opp.end();
List<Suggestion> result = provider.completionSuggestions(
"", cursor - pastSpace, anchor);
anchor[0] += pastSpace;
return result;
}
return Collections.emptyList();
};
}
// /history command completion
private static CompletionProvider historyCompletion() {
return optionCompletion(HISTORY_OPTION_COMPLETION_PROVIDER);
}
// /reload command completion
private static CompletionProvider reloadCompletion() {
return optionCompletion(RELOAD_OPTIONS_COMPLETION_PROVIDER);
}
// /env command completion
private static CompletionProvider envCompletion() {
return optionCompletion(COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER);
}
private static CompletionProvider orMostSpecificCompletion(
CompletionProvider left, CompletionProvider right) {
return (code, cursor, anchor) -> {
int[] leftAnchor = {-1};
int[] rightAnchor = {-1};
List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor);
List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor);
List<Suggestion> suggestions = new ArrayList<>();
if (leftAnchor[0] >= rightAnchor[0]) {
anchor[0] = leftAnchor[0];
suggestions.addAll(leftSuggestions);
}
if (leftAnchor[0] <= rightAnchor[0]) {
anchor[0] = rightAnchor[0];
suggestions.addAll(rightSuggestions);
}
return suggestions;
};
}
// Snippet lists
Stream<Snippet> allSnippets() {
return state.snippets();
}
Stream<Snippet> dropableSnippets() {
return state.snippets()
.filter(sn -> state.status(sn).isActive());
}
Stream<VarSnippet> allVarSnippets() {
return state.snippets()
.filter(sn -> sn.kind() == Snippet.Kind.VAR)
.map(sn -> (VarSnippet) sn);
}
Stream<MethodSnippet> allMethodSnippets() {
return state.snippets()
.filter(sn -> sn.kind() == Snippet.Kind.METHOD)
.map(sn -> (MethodSnippet) sn);
}
Stream<TypeDeclSnippet> allTypeSnippets() {
return state.snippets()
.filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL)
.map(sn -> (TypeDeclSnippet) sn);
}
// Table of commands -- with command forms, argument kinds, helpKey message, implementation, ...
{
registerCommand(new Command("/list",
this::cmdList,
snippetWithOptionCompletion(SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER,
this::allSnippets)));
registerCommand(new Command("/edit",
this::cmdEdit,
snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
this::allSnippets)));
registerCommand(new Command("/drop",
this::cmdDrop,
snippetCompletion(this::dropableSnippets),
CommandKind.REPLAY));
registerCommand(new Command("/save",
this::cmdSave,
saveCompletion()));
registerCommand(new Command("/open",
this::cmdOpen,
FILE_COMPLETION_PROVIDER));
registerCommand(new Command("/vars",
this::cmdVars,
snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
this::allVarSnippets)));
registerCommand(new Command("/methods",
this::cmdMethods,
snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
this::allMethodSnippets)));
registerCommand(new Command("/types",
this::cmdTypes,
snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
this::allTypeSnippets)));
registerCommand(new Command("/imports",
arg -> cmdImports(),
EMPTY_COMPLETION_PROVIDER));
registerCommand(new Command("/exit",
arg -> cmdExit(arg),
(sn, c, a) -> {
if (analysis == null || sn.isEmpty()) {
// No completions if uninitialized or snippet not started
return Collections.emptyList();
} else {
// Give exit code an int context by prefixing the arg
List<Suggestion> suggestions = analysis.completionSuggestions(INT_PREFIX + sn,
INT_PREFIX.length() + c, a);
a[0] -= INT_PREFIX.length();
return suggestions;
}
}));
registerCommand(new Command("/env",
arg -> cmdEnv(arg),
envCompletion()));
registerCommand(new Command("/reset",
arg -> cmdReset(arg),
envCompletion()));
registerCommand(new Command("/reload",
this::cmdReload,
reloadCompletion()));
registerCommand(new Command("/history",
this::cmdHistory,
historyCompletion()));
registerCommand(new Command("/debug",
this::cmdDebug,
EMPTY_COMPLETION_PROVIDER,
CommandKind.HIDDEN));
registerCommand(new Command("/help",
this::cmdHelp,
helpCompletion()));
registerCommand(new Command("/set",
this::cmdSet,
new ContinuousCompletionProvider(Map.of(
// need more completion for format for usability
"format", feedback.modeCompletions(),
"truncation", feedback.modeCompletions(),
"feedback", feedback.modeCompletions(),
"mode", skipWordThenCompletion(orMostSpecificCompletion(
feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER),
SET_MODE_OPTIONS_COMPLETION_PROVIDER)),
"prompt", feedback.modeCompletions(),
"editor", fileCompletions(Files::isExecutable),
"start", FILE_COMPLETION_PROVIDER),
STARTSWITH_MATCHER)));
registerCommand(new Command("/?",
"help.quest",
this::cmdHelp,
helpCompletion(),
CommandKind.NORMAL));
registerCommand(new Command("/!",
"help.bang",
arg -> cmdUseHistoryEntry(-1),
EMPTY_COMPLETION_PROVIDER,
CommandKind.NORMAL));
// Documentation pseudo-commands
registerCommand(new Command("/<id>",
"help.slashID",
arg -> cmdHelp("rerun"),
EMPTY_COMPLETION_PROVIDER,
CommandKind.HELP_ONLY));
registerCommand(new Command("/-<n>",
"help.previous",
arg -> cmdHelp("rerun"),
EMPTY_COMPLETION_PROVIDER,
CommandKind.HELP_ONLY));
registerCommand(new Command("intro",
"help.intro",
CommandKind.HELP_SUBJECT));
registerCommand(new Command("keys",
"help.keys",
CommandKind.HELP_SUBJECT));
registerCommand(new Command("id",
"help.id",
CommandKind.HELP_SUBJECT));
registerCommand(new Command("shortcuts",
"help.shortcuts",
CommandKind.HELP_SUBJECT));
registerCommand(new Command("context",
"help.context",
CommandKind.HELP_SUBJECT));
registerCommand(new Command("rerun",
"help.rerun",
CommandKind.HELP_SUBJECT));
commandCompletions = new ContinuousCompletionProvider(
commands.values().stream()
.filter(c -> c.kind.shouldSuggestCompletions)
.collect(toMap(c -> c.command, c -> c.completions)),
STARTSWITH_MATCHER);
}
private ContinuousCompletionProvider commandCompletions;
public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
return commandCompletions.completionSuggestions(code, cursor, anchor);
}
public List<String> commandDocumentation(String code, int cursor, boolean shortDescription) {
code = code.substring(0, cursor).replaceAll("\\h+", " ");
String stripped = code.replaceFirst("/(he(lp?)?|\\?) ", "");
boolean inHelp = !code.equals(stripped);
int space = stripped.indexOf(' ');
String prefix = space != (-1) ? stripped.substring(0, space) : stripped;
List<String> result = new ArrayList<>();
List<Entry<String, String>> toShow;
if (SET_SUB.matcher(stripped).matches()) {
String setSubcommand = stripped.replaceFirst("/?set ([^ ]*)($| .*)", "$1");
toShow =
Arrays.stream(SET_SUBCOMMANDS)
.filter(s -> s.startsWith(setSubcommand))
.map(s -> new SimpleEntry<>("/set " + s, "help.set." + s))
.collect(toList());
} else if (RERUN_ID.matcher(stripped).matches()) {
toShow =
singletonList(new SimpleEntry<>("/<id>", "help.rerun"));
} else if (RERUN_PREVIOUS.matcher(stripped).matches()) {
toShow =
singletonList(new SimpleEntry<>("/-<n>", "help.rerun"));
} else {
toShow =
commands.values()
.stream()
.filter(c -> c.command.startsWith(prefix)
|| c.command.substring(1).startsWith(prefix))
.filter(c -> c.kind.showInHelp
|| (inHelp && c.kind == CommandKind.HELP_SUBJECT))
.sorted((c1, c2) -> c1.command.compareTo(c2.command))
.map(c -> new SimpleEntry<>(c.command, c.helpKey))
.collect(toList());
}
if (toShow.size() == 1 && !inHelp) {
result.add(getResourceString(toShow.get(0).getValue() + (shortDescription ? ".summary" : "")));
} else {
for (Entry<String, String> e : toShow) {
result.add(e.getKey() + "\n" + getResourceString(e.getValue() + (shortDescription ? ".summary" : "")));
}
}
return result;
}
// Attempt to stop currently running evaluation
void stop() {
state.stop();
}
// --- Command implementations ---
private static final String[] SET_SUBCOMMANDS = new String[]{
"format", "truncation", "feedback", "mode", "prompt", "editor", "start"};
final boolean cmdSet(String arg) {
String cmd = "/set";
ArgTokenizer at = new ArgTokenizer(cmd, arg.trim());
String which = subCommand(cmd, at, SET_SUBCOMMANDS);
if (which == null) {
return false;
}
switch (which) {
case "_retain": {
errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole());
return false;
}
case "_blank": {
// show top-level settings
new SetEditor().set();
showSetStart();
setFeedback(this, at); // no args so shows feedback setting
hardmsg("jshell.msg.set.show.mode.settings");
return true;
}
case "format":
return feedback.setFormat(this, at);
case "truncation":
return feedback.setTruncation(this, at);
case "feedback":
return setFeedback(this, at);
case "mode":
return feedback.setMode(this, at,
retained -> prefs.put(MODE_KEY, retained));
case "prompt":
return feedback.setPrompt(this, at);
case "editor":
return new SetEditor(at).set();
case "start":
return setStart(at);
default:
errormsg("jshell.err.arg", cmd, at.val());
return false;
}
}
boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) {
return feedback.setFeedback(messageHandler, at,
fb -> prefs.put(FEEDBACK_KEY, fb));
}
// Find which, if any, sub-command matches.
// Return null on error
String subCommand(String cmd, ArgTokenizer at, String[] subs) {
at.allowedOptions("-retain");
String sub = at.next();
if (sub == null) {
// No sub-command was given
return at.hasOption("-retain")
? "_retain"
: "_blank";
}
String[] matches = Arrays.stream(subs)
.filter(s -> s.startsWith(sub))
.toArray(String[]::new);
if (matches.length == 0) {
// There are no matching sub-commands
errormsg("jshell.err.arg", cmd, sub);
fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs)
.collect(Collectors.joining(", "))
);
return null;
}
if (matches.length > 1) {
// More than one sub-command matches the initial characters provided
errormsg("jshell.err.sub.ambiguous", cmd, sub);
fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches)
.collect(Collectors.joining(", "))
);
return null;
}
return matches[0];
}
static class EditorSetting {
static String BUILT_IN_REP = "-default";
static char WAIT_PREFIX = '-';
static char NORMAL_PREFIX = '*';
final String[] cmd;
final boolean wait;
EditorSetting(String[] cmd, boolean wait) {
this.wait = wait;
this.cmd = cmd;
}
// returns null if not stored in preferences
static EditorSetting fromPrefs(PersistentStorage prefs) {
// Read retained editor setting (if any)
String editorString = prefs.get(EDITOR_KEY);
if (editorString == null || editorString.isEmpty()) {
return null;
} else if (editorString.equals(BUILT_IN_REP)) {
return BUILT_IN_EDITOR;
} else {
boolean wait = false;
char waitMarker = editorString.charAt(0);
if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) {
wait = waitMarker == WAIT_PREFIX;
editorString = editorString.substring(1);
}
String[] cmd = editorString.split(RECORD_SEPARATOR);
return new EditorSetting(cmd, wait);
}
}
static void removePrefs(PersistentStorage prefs) {
prefs.remove(EDITOR_KEY);
}
void toPrefs(PersistentStorage prefs) {
prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR)
? BUILT_IN_REP
: (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd));
}
@Override
public boolean equals(Object o) {
if (o instanceof EditorSetting) {
EditorSetting ed = (EditorSetting) o;
return Arrays.equals(cmd, ed.cmd) && wait == ed.wait;
} else {
return false;
}
}
@Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Arrays.deepHashCode(this.cmd);
hash = 71 * hash + (this.wait ? 1 : 0);
return hash;
}
}
class SetEditor {
private final ArgTokenizer at;
private final String[] command;
private final boolean hasCommand;
private final boolean defaultOption;
private final boolean deleteOption;
private final boolean waitOption;
private final boolean retainOption;
private final int primaryOptionCount;
SetEditor(ArgTokenizer at) {
at.allowedOptions("-default", "-wait", "-retain", "-delete");
String prog = at.next();
List<String> ed = new ArrayList<>();
while (at.val() != null) {
ed.add(at.val());
at.nextToken(); // so that options are not interpreted as jshell options
}
this.at = at;
this.command = ed.toArray(new String[ed.size()]);
this.hasCommand = command.length > 0;
this.defaultOption = at.hasOption("-default");
this.deleteOption = at.hasOption("-delete");
this.waitOption = at.hasOption("-wait");
this.retainOption = at.hasOption("-retain");
this.primaryOptionCount = (hasCommand? 1 : 0) + (defaultOption? 1 : 0) + (deleteOption? 1 : 0);
}
SetEditor() {
this(new ArgTokenizer("", ""));
}
boolean set() {
if (!check()) {
return false;
}
if (primaryOptionCount == 0 && !retainOption) {
// No settings or -retain, so this is a query
EditorSetting retained = EditorSetting.fromPrefs(prefs);
if (retained != null) {
// retained editor is set
hard("/set editor -retain %s", format(retained));
}
if (retained == null || !retained.equals(editor)) {
// editor is not retained or retained is different from set
hard("/set editor %s", format(editor));
}
return true;
}
if (retainOption && deleteOption) {
EditorSetting.removePrefs(prefs);
}
install();
if (retainOption && !deleteOption) {
editor.toPrefs(prefs);
fluffmsg("jshell.msg.set.editor.retain", format(editor));
}
return true;
}
private boolean check() {
if (!checkOptionsAndRemainingInput(at)) {
return false;
}
if (primaryOptionCount > 1) {
errormsg("jshell.err.default.option.or.program", at.whole());
return false;
}
if (waitOption && !hasCommand) {
errormsg("jshell.err.wait.applies.to.external.editor", at.whole());
return false;
}
return true;
}
private void install() {
if (hasCommand) {
editor = new EditorSetting(command, waitOption);
} else if (defaultOption) {
editor = BUILT_IN_EDITOR;
} else if (deleteOption) {
configEditor();
} else {
return;
}
fluffmsg("jshell.msg.set.editor.set", format(editor));
}
private String format(EditorSetting ed) {
if (ed == BUILT_IN_EDITOR) {
return "-default";
} else {
Stream<String> elems = Arrays.stream(ed.cmd);
if (ed.wait) {
elems = Stream.concat(Stream.of("-wait"), elems);
}
return elems.collect(joining(" "));
}
}
}
// The sub-command: /set start <start-file>
boolean setStart(ArgTokenizer at) {
at.allowedOptions("-default", "-none", "-retain");
List<String> fns = new ArrayList<>();
while (at.next() != null) {
fns.add(at.val());
}
if (!checkOptionsAndRemainingInput(at)) {
return false;
}
boolean defaultOption = at.hasOption("-default");
boolean noneOption = at.hasOption("-none");
boolean retainOption = at.hasOption("-retain");
boolean hasFile = !fns.isEmpty();
int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0);
if (argCount > 1) {
errormsg("jshell.err.option.or.filename", at.whole());
return false;
}
if (argCount == 0 && !retainOption) {
// no options or filename, show current setting
showSetStart();
return true;
}
if (hasFile) {
startup = Startup.fromFileList(fns, "/set start", this);
if (startup == null) {
return false;
}
} else if (defaultOption) {
startup = Startup.defaultStartup(this);
} else if (noneOption) {
startup = Startup.noStartup();
}
if (retainOption) {
// retain startup setting
prefs.put(STARTUP_KEY, startup.storedForm());
}
return true;
}
// show the "/set start" settings (retained and, if different, current)
// as commands (and file contents). All commands first, then contents.
void showSetStart() {
StringBuilder sb = new StringBuilder();
String retained = prefs.get(STARTUP_KEY);
if (retained != null) {
Startup retainedStart = Startup.unpack(retained, this);
boolean currentDifferent = !startup.equals(retainedStart);
sb.append(retainedStart.show(true));
if (currentDifferent) {
sb.append(startup.show(false));
}
sb.append(retainedStart.showDetail());
if (currentDifferent) {
sb.append(startup.showDetail());
}
} else {
sb.append(startup.show(false));
sb.append(startup.showDetail());
}
hard(sb.toString());
}
boolean cmdDebug(String arg) {
if (arg.isEmpty()) {
debug = !debug;
InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0);
fluff("Debugging %s", debug ? "on" : "off");
} else {
for (char ch : arg.toCharArray()) {
switch (ch) {
case '0':
debugFlags = 0;
debug = false;
fluff("Debugging off");
break;
case 'r':
debug = true;
fluff("REPL tool debugging on");
break;
case 'g':
debugFlags |= DBG_GEN;
fluff("General debugging on");
break;
case 'f':
debugFlags |= DBG_FMGR;
fluff("File manager debugging on");
break;
case 'c':
debugFlags |= DBG_COMPA;
fluff("Completion analysis debugging on");
break;
case 'd':
debugFlags |= DBG_DEP;
fluff("Dependency debugging on");
break;
case 'e':
debugFlags |= DBG_EVNT;
fluff("Event debugging on");
break;
case 'w':
debugFlags |= DBG_WRAP;
fluff("Wrap debugging on");
break;
case 'b':
cmdout.printf("RemoteVM Options: %s\nCompiler options: %s\n",
Arrays.toString(options.remoteVmOptions()),
Arrays.toString(options.compilerOptions()));
break;
default:
error("Unknown debugging option: %c", ch);
fluff("Use: 0 r g f c d e w b");
return false;
}
}
InternalDebugControl.setDebugFlags(state, debugFlags);
}
return true;
}
private boolean cmdExit(String arg) {
if (!arg.trim().isEmpty()) {
debug("Compiling exit: %s", arg);
List<SnippetEvent> events = state.eval(arg);
for (SnippetEvent e : events) {
// Only care about main snippet
if (e.causeSnippet() == null) {
Snippet sn = e.snippet();
// Show any diagnostics
List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
String source = sn.source();
displayDiagnostics(source, diagnostics);
// Show any exceptions
if (e.exception() != null && e.status() != Status.REJECTED) {
if (displayException(e.exception())) {
// Abort: an exception occurred (reported)
return false;
}
}
if (e.status() != Status.VALID) {
// Abort: can only use valid snippets, diagnostics have been reported (above)
return false;
}
String typeName;
if (sn.kind() == Kind.EXPRESSION) {
typeName = ((ExpressionSnippet) sn).typeName();
} else if (sn.subKind() == TEMP_VAR_EXPRESSION_SUBKIND) {
typeName = ((VarSnippet) sn).typeName();
} else {
// Abort: not an expression
errormsg("jshell.err.exit.not.expression", arg);
return false;
}
switch (typeName) {
case "int":
case "Integer":
case "byte":
case "Byte":
case "short":
case "Short":
try {
int i = Integer.parseInt(e.value());
/**
addToReplayHistory("/exit " + arg);
replayableHistory.storeHistory(prefs);
closeState();
try {
input.close();
} catch (Exception exc) {
// ignore
}
* **/
exitCode = i;
break;
} catch (NumberFormatException exc) {
// Abort: bad value
errormsg("jshell.err.exit.bad.value", arg, e.value());
return false;
}
default:
// Abort: bad type
errormsg("jshell.err.exit.bad.type", arg, typeName);
return false;
}
}
}
}
regenerateOnDeath = false;
live = false;
if (exitCode == 0) {
fluffmsg("jshell.msg.goodbye");
} else {
fluffmsg("jshell.msg.goodbye.value", exitCode);
}
return true;
}
boolean cmdHelp(String arg) {
ArgTokenizer at = new ArgTokenizer("/help", arg);
String subject = at.next();
if (subject != null) {
// check if the requested subject is a help subject or
// a command, with or without slash
Command[] matches = commands.values().stream()
.filter(c -> c.command.startsWith(subject)
|| c.command.substring(1).startsWith(subject))
.toArray(Command[]::new);
if (matches.length == 1) {
String cmd = matches[0].command;
if (cmd.equals("/set")) {
// Print the help doc for the specified sub-command
String which = subCommand(cmd, at, SET_SUBCOMMANDS);
if (which == null) {
return false;
}
if (!which.equals("_blank")) {
printHelp("/set " + which, "help.set." + which);
return true;
}
}
}
if (matches.length > 0) {
for (Command c : matches) {
printHelp(c.command, c.helpKey);
}
return true;
} else {
// failing everything else, check if this is the start of
// a /set sub-command name
String[] subs = Arrays.stream(SET_SUBCOMMANDS)
.filter(s -> s.startsWith(subject))
.toArray(String[]::new);
if (subs.length > 0) {
for (String sub : subs) {
printHelp("/set " + sub, "help.set." + sub);
}
return true;
}
errormsg("jshell.err.help.arg", arg);
}
}
hardmsg("jshell.msg.help.begin");
hardPairs(commands.values().stream()
.filter(cmd -> cmd.kind.showInHelp),
cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"),
cmd -> getResourceString(cmd.helpKey + ".summary")
);
hardmsg("jshell.msg.help.subject");
hardPairs(commands.values().stream()
.filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT),
cmd -> cmd.command,
cmd -> getResourceString(cmd.helpKey + ".summary")
);
return true;
}
private void printHelp(String name, String key) {
int len = name.length();
String centered = "%" + ((OUTPUT_WIDTH + len) / 2) + "s";
hard("");
hard(centered, name);
hard(centered, Stream.generate(() -> "=").limit(len).collect(Collectors.joining()));
hard("");
hardrb(key);
}
private boolean cmdHistory(String rawArgs) {
ArgTokenizer at = new ArgTokenizer("/history", rawArgs.trim());
at.allowedOptions("-all");
if (!checkOptionsAndRemainingInput(at)) {
return false;
}
cmdout.println();
for (String s : input.history(!at.hasOption("-all"))) {
// No number prefix, confusing with snippet ids
cmdout.printf("%s\n", s);
}
return true;
}
/**
* Avoid parameterized varargs possible heap pollution warning.
*/
private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { }
/**
* Apply filters to a stream until one that is non-empty is found.
* Adapted from Stuart Marks
*
* @param supplier Supply the Snippet stream to filter
* @param filters Filters to attempt
* @return The non-empty filtered Stream, or null
*/
@SafeVarargs
private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier,
SnippetPredicate<T>... filters) {
for (SnippetPredicate<T> filt : filters) {
Iterator<T> iterator = supplier.get().filter(filt).iterator();
if (iterator.hasNext()) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
}
}
return null;
}
private boolean inStartUp(Snippet sn) {
return mapSnippet.get(sn).space == startNamespace;
}
private boolean isActive(Snippet sn) {
return state.status(sn).isActive();
}
private boolean mainActive(Snippet sn) {
return !inStartUp(sn) && isActive(sn);
}
private boolean matchingDeclaration(Snippet sn, String name) {
return sn instanceof DeclarationSnippet
&& ((DeclarationSnippet) sn).name().equals(name);
}
/**
* Convert user arguments to a Stream of snippets referenced by those
* arguments (or lack of arguments).
*
* @param snippets the base list of possible snippets
* @param defFilter the filter to apply to the arguments if no argument
* @param rawargs the user's argument to the command, maybe be the empty
* string
* @return a Stream of referenced snippets or null if no matches are found
*/
private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
Predicate<Snippet> defFilter, String rawargs, String cmd) {
ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim());
at.allowedOptions("-all", "-start");
return argsOptionsToSnippets(snippetSupplier, defFilter, at);
}
/**
* Convert user arguments to a Stream of snippets referenced by those
* arguments (or lack of arguments).
*
* @param snippets the base list of possible snippets
* @param defFilter the filter to apply to the arguments if no argument
* @param at the ArgTokenizer, with allowed options set
* @return
*/
private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
Predicate<Snippet> defFilter, ArgTokenizer at) {
List<String> args = new ArrayList<>();
String s;
while ((s = at.next()) != null) {
args.add(s);
}
if (!checkOptionsAndRemainingInput(at)) {
return null;
}
if (at.optionCount() > 0 && args.size() > 0) {
errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole());
return null;
}
if (at.optionCount() > 1) {
errormsg("jshell.err.conflicting.options", at.whole());
return null;
}
if (at.isAllowedOption("-all") && at.hasOption("-all")) {
// all snippets including start-up, failed, and overwritten
return snippetSupplier.get();
}
if (at.isAllowedOption("-start") && at.hasOption("-start")) {
// start-up snippets
return snippetSupplier.get()
.filter(this::inStartUp);
}
if (args.isEmpty()) {
// Default is all active user snippets
return snippetSupplier.get()
.filter(defFilter);
}
return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args);
}
/**
* Support for converting arguments that are definition names, snippet ids,
* or snippet id ranges into a stream of snippets,
*
* @param <T> the snipper subtype
*/
private class ArgToSnippets<T extends Snippet> {
// the supplier of snippet streams
final Supplier<Stream<T>> snippetSupplier;
// these two are parallel, and lazily filled if a range is encountered
List<T> allSnippets;
String[] allIds = null;
/**
*
* @param snippetSupplier the base list of possible snippets
*/
ArgToSnippets(Supplier<Stream<T>> snippetSupplier) {
this.snippetSupplier = snippetSupplier;
}
/**
* Convert user arguments to a Stream of snippets referenced by those
* arguments.
*
* @param args the user's argument to the command, maybe be the empty
* list
* @return a Stream of referenced snippets or null if no matches to
* specific arg
*/
Stream<T> argsToSnippets(List<String> args) {
Stream<T> result = null;
for (String arg : args) {
// Find the best match
Stream<T> st = argToSnippets(arg);
if (st == null) {
return null;
} else {
result = (result == null)
? st
: Stream.concat(result, st);
}
}
return result;
}
/**
* Convert a user argument to a Stream of snippets referenced by the
* argument.
*
* @param snippetSupplier the base list of possible snippets
* @param arg the user's argument to the command
* @return a Stream of referenced snippets or null if no matches to
* specific arg
*/
Stream<T> argToSnippets(String arg) {
if (arg.contains("-")) {
return range(arg);
}
// Find the best match
Stream<T> st = layeredSnippetSearch(snippetSupplier, arg);
if (st == null) {
badSnippetErrormsg(arg);
return null;
} else {
return st;
}
}
/**
* Look for inappropriate snippets to give best error message
*
* @param arg the bad snippet arg
* @param errKey the not found error key
*/
void badSnippetErrormsg(String arg) {
Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
if (est == null) {
if (ID.matcher(arg).matches()) {
errormsg("jshell.err.no.snippet.with.id", arg);
} else {
errormsg("jshell.err.no.such.snippets", arg);
}
} else {
errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command",
arg, est.findFirst().get().source());
}
}
/**
* Search through the snippets for the best match to the id/name.
*
* @param <R> the snippet type
* @param aSnippetSupplier the supplier of snippet streams
* @param arg the arg to match
* @return a Stream of referenced snippets or null if no matches to
* specific arg
*/
<R extends Snippet> Stream<R> layeredSnippetSearch(Supplier<Stream<R>> aSnippetSupplier, String arg) {
return nonEmptyStream(
// the stream supplier
aSnippetSupplier,
// look for active user declarations matching the name
sn -> isActive(sn) && matchingDeclaration(sn, arg),
// else, look for any declarations matching the name
sn -> matchingDeclaration(sn, arg),
// else, look for an id of this name
sn -> sn.id().equals(arg)
);
}
/**
* Given an id1-id2 range specifier, return a stream of snippets within
* our context
*
* @param arg the range arg
* @return a Stream of referenced snippets or null if no matches to
* specific arg
*/
Stream<T> range(String arg) {
int dash = arg.indexOf('-');
String iid = arg.substring(0, dash);
String tid = arg.substring(dash + 1);
int iidx = snippetIndex(iid);
if (iidx < 0) {
return null;
}
int tidx = snippetIndex(tid);
if (tidx < 0) {
return null;
}
if (tidx < iidx) {
errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid);
return null;
}
return allSnippets.subList(iidx, tidx+1).stream();
}
/**
* Lazily initialize the id mapping -- needed only for id ranges.
*/
void initIdMapping() {
if (allIds == null) {
allSnippets = snippetSupplier.get()
.sorted((a, b) -> order(a) - order(b))
.collect(toList());
allIds = allSnippets.stream()
.map(sn -> sn.id())
.toArray(n -> new String[n]);
}
}
/**
* Return all the snippet ids -- within the context, and in order.
*
* @return the snippet ids
*/
String[] allIds() {
initIdMapping();
return allIds;
}
/**
* Establish an order on snippet ids. All startup snippets are first,
* all error snippets are last -- within that is by snippet number.
*
* @param id the id string
* @return an ordering int
*/
int order(String id) {
try {
switch (id.charAt(0)) {
case 's':
return Integer.parseInt(id.substring(1));
case 'e':
return 0x40000000 + Integer.parseInt(id.substring(1));
default:
return 0x20000000 + Integer.parseInt(id);
}
} catch (Exception ex) {
return 0x60000000;
}
}
/**
* Establish an order on snippets, based on its snippet id. All startup
* snippets are first, all error snippets are last -- within that is by
* snippet number.
*
* @param sn the id string
* @return an ordering int
*/
int order(Snippet sn) {
return order(sn.id());
}
/**
* Find the index into the parallel allSnippets and allIds structures.
*
* @param s the snippet id name
* @return the index, or, if not found, report the error and return a
* negative number
*/
int snippetIndex(String s) {
int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s,
(a, b) -> order(a) - order(b));
if (idx < 0) {
// the id is not in the snippet domain, find the right error to report
if (!ID.matcher(s).matches()) {
errormsg("jshell.err.range.requires.id", s);
} else {
badSnippetErrormsg(s);
}
}
return idx;
}
}
private boolean cmdDrop(String rawargs) {
ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim());
at.allowedOptions();
List<String> args = new ArrayList<>();
String s;
while ((s = at.next()) != null) {
args.add(s);
}
if (!checkOptionsAndRemainingInput(at)) {
return false;
}
if (args.isEmpty()) {
errormsg("jshell.err.drop.arg");
return false;
}
Stream<Snippet> stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args);
if (stream == null) {
// Snippet not found. Error already printed
fluffmsg("jshell.msg.see.classes.etc");
return false;
}
stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent));
return true;
}
private boolean cmdEdit(String arg) {
Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
this::mainActive, arg, "/edit");
if (stream == null) {
return false;
}
Set<String> srcSet = new LinkedHashSet<>();
stream.forEachOrdered(sn -> {
String src = sn.source();
switch (sn.subKind()) {
case VAR_VALUE_SUBKIND:
break;
case ASSIGNMENT_SUBKIND:
case OTHER_EXPRESSION_SUBKIND:
case TEMP_VAR_EXPRESSION_SUBKIND:
case UNKNOWN_SUBKIND:
if (!src.endsWith(";")) {
src = src + ";";
}
srcSet.add(src);
break;
case STATEMENT_SUBKIND:
if (src.endsWith("}")) {
// Could end with block or, for example, new Foo() {...}
// so, we need deeper analysis to know if it needs a semicolon
src = analysis.analyzeCompletion(src).source();
} else if (!src.endsWith(";")) {
src = src + ";";
}
srcSet.add(src);
break;
default:
srcSet.add(src);
break;
}
});
StringBuilder sb = new StringBuilder();
for (String s : srcSet) {
sb.append(s);
sb.append('\n');
}
String src = sb.toString();
Consumer<String> saveHandler = new SaveHandler(src, srcSet);
Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
if (editor == BUILT_IN_EDITOR) {
return builtInEdit(src, saveHandler, errorHandler);
} else {
// Changes have occurred in temp edit directory,
// transfer the new sources to JShell (unless the editor is
// running directly in JShell's window -- don't make a mess)
String[] buffer = new String[1];
Consumer<String> extSaveHandler = s -> {
if (input.terminalEditorRunning()) {
buffer[0] = s;
} else {
saveHandler.accept(s);
}
};
ExternalEditor.edit(editor.cmd, src,
errorHandler, extSaveHandler,
() -> input.suspend(),
() -> input.resume(),
editor.wait,
() -> hardrb("jshell.msg.press.return.to.leave.edit.mode"));
if (buffer[0] != null) {
saveHandler.accept(buffer[0]);
}
}
return true;
}
//where
// start the built-in editor
private boolean builtInEdit(String initialText,
Consumer<String> saveHandler, Consumer<String> errorHandler) {
try {
ServiceLoader<BuildInEditorProvider> sl
= ServiceLoader.load(BuildInEditorProvider.class);
// Find the highest ranking provider
BuildInEditorProvider provider = null;
for (BuildInEditorProvider p : sl) {
if (provider == null || p.rank() > provider.rank()) {
provider = p;
}
}
if (provider != null) {
provider.edit(getResourceString("jshell.label.editpad"),
initialText, saveHandler, errorHandler);
return true;
} else {
errormsg("jshell.err.no.builtin.editor");
}
} catch (RuntimeException ex) {
errormsg("jshell.err.cant.launch.editor", ex);
}
fluffmsg("jshell.msg.try.set.editor");
return false;
}
//where
// receives editor requests to save
private class SaveHandler implements Consumer<String> {
String src;
Set<String> currSrcs;
SaveHandler(String src, Set<String> ss) {
this.src = src;
this.currSrcs = ss;
}
@Override
public void accept(String s) {
if (!s.equals(src)) { // quick check first
src = s;
try {
Set<String> nextSrcs = new LinkedHashSet<>();
boolean failed = false;
while (true) {
CompletionInfo an = analysis.analyzeCompletion(s);
if (!an.completeness().isComplete()) {
break;
}
String tsrc = trimNewlines(an.source());
if (!failed && !currSrcs.contains(tsrc)) {
failed = !processSource(tsrc);
}
nextSrcs.add(tsrc);
if (an.remaining().isEmpty()) {
break;
}
s = an.remaining();
}
currSrcs = nextSrcs;
} catch (IllegalStateException ex) {
errormsg("jshell.msg.resetting");
resetState();
currSrcs = new LinkedHashSet<>(); // re-process everything
}
}
}
private String trimNewlines(String s) {
int b = 0;
while (b < s.length() && s.charAt(b) == '\n') {
++b;
}
int e = s.length() -1;
while (e >= 0 && s.charAt(e) == '\n') {
--e;
}
return s.substring(b, e + 1);
}
}
private boolean cmdList(String arg) {
if (arg.length() >= 2 && "-history".startsWith(arg)) {
return cmdHistory("");
}
Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
this::mainActive, arg, "/list");
if (stream == null) {
return false;
}
// prevent double newline on empty list
boolean[] hasOutput = new boolean[1];
stream.forEachOrdered(sn -> {
if (!hasOutput[0]) {
cmdout.println();
hasOutput[0] = true;
}
cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n "));
});
return true;
}
private boolean cmdOpen(String filename) {
return runFile(filename, "/open");
}
private boolean runFile(String filename, String context) {
if (!filename.isEmpty()) {
try {
Scanner scanner;
if (!interactiveModeBegun && filename.equals("-")) {
// - on command line: no interactive later, read from input
regenerateOnDeath = false;
scanner = new Scanner(cmdin);
} else {
Path path = null;
URL url = null;
String resource;
try {
path = toPathResolvingUserHome(filename);
} catch (InvalidPathException ipe) {
try {
url = new URL(filename);
if (url.getProtocol().equalsIgnoreCase("file")) {
path = Paths.get(url.toURI());
}
} catch (MalformedURLException | URISyntaxException e) {
throw new FileNotFoundException(filename);
}
}
if (path != null && Files.exists(path)) {
scanner = new Scanner(new FileReader(path.toString()));
} else if ((resource = getResource(filename)) != null) {
scanner = new Scanner(new StringReader(resource));
} else {
if (url == null) {
try {
url = new URL(filename);
} catch (MalformedURLException mue) {
throw new FileNotFoundException(filename);
}
}
scanner = new Scanner(url.openStream());
}
}
try (var scannerIOContext = new ScannerIOContext(scanner)) {
run(scannerIOContext);
}
return true;
} catch (FileNotFoundException e) {
errormsg("jshell.err.file.not.found", context, filename, e.getMessage());
} catch (Exception e) {
errormsg("jshell.err.file.exception", context, filename, e);
}
} else {
errormsg("jshell.err.file.filename", context);
}
return false;
}
static String getResource(String name) {
if (BUILTIN_FILE_PATTERN.matcher(name).matches()) {
try {
return readResource(name);
} catch (Throwable t) {
// Fall-through to null
}
}
return null;
}
// Read a built-in file from resources or compute it
static String readResource(String name) throws Exception {
// Class to compute imports by following requires for a module
class ComputeImports {
final String base;
ModuleFinder finder = ModuleFinder.ofSystem();
ComputeImports(String base) {
this.base = base;
}
Set<ModuleDescriptor> modules() {
Set<ModuleDescriptor> closure = new HashSet<>();
moduleClosure(finder.find(base), closure);
return closure;
}
void moduleClosure(Optional<ModuleReference> omr, Set<ModuleDescriptor> closure) {
if (omr.isPresent()) {
ModuleDescriptor mdesc = omr.get().descriptor();
if (closure.add(mdesc)) {
for (ModuleDescriptor.Requires req : mdesc.requires()) {
if (!req.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) {
moduleClosure(finder.find(req.name()), closure);
}
}
}
}
}
Set<String> packages() {
return modules().stream().flatMap(md -> md.exports().stream())
.filter(e -> !e.isQualified()).map(Object::toString).collect(Collectors.toSet());
}
String imports() {
Set<String> si = packages();
String[] ai = si.toArray(new String[si.size()]);
Arrays.sort(ai);
return Arrays.stream(ai)
.map(p -> String.format("import %s.*;\n", p))
.collect(Collectors.joining());
}
}
if (name.equals("JAVASE")) {
// The built-in JAVASE is computed as the imports of all the packages in Java SE
return new ComputeImports("java.se").imports();
}
// Attempt to find the file as a resource
String spec = String.format(BUILTIN_FILE_PATH_FORMAT, name);
try (InputStream in = JShellTool.class.getResourceAsStream(spec);
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
return reader.lines().collect(Collectors.joining("\n", "", "\n"));
}
}
private boolean cmdReset(String rawargs) {
Options oldOptions = rawargs.trim().isEmpty()? null : options;
if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
return false;
}
live = false;
fluffmsg("jshell.msg.resetting.state");
return doReload(null, false, oldOptions);
}
private boolean cmdReload(String rawargs) {
Options oldOptions = rawargs.trim().isEmpty()? null : options;
OptionParserReload ap = new OptionParserReload();
if (!parseCommandLineLikeFlags(rawargs, ap)) {
return false;
}
ReplayableHistory history;
if (ap.restore()) {
if (replayableHistoryPrevious == null) {
errormsg("jshell.err.reload.no.previous");
return false;
}
history = replayableHistoryPrevious;
fluffmsg("jshell.err.reload.restarting.previous.state");
} else {
history = replayableHistory;
fluffmsg("jshell.err.reload.restarting.state");
}
boolean success = doReload(history, !ap.quiet(), oldOptions);
if (success && ap.restore()) {
// if we are restoring from previous, then if nothing was added
// before time of exit, there is nothing to save
replayableHistory.markSaved();
}
return success;
}
private boolean cmdEnv(String rawargs) {
if (rawargs.trim().isEmpty()) {
// No arguments, display current settings (as option flags)
StringBuilder sb = new StringBuilder();
for (String a : options.shownOptions()) {
sb.append(
a.startsWith("-")
? sb.length() > 0
? "\n "
: " "
: " ");
sb.append(a);
}
if (sb.length() > 0) {
hard(sb.toString());
}
return false;
}
Options oldOptions = options;
if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
return false;
}
fluffmsg("jshell.msg.set.restore");
return doReload(replayableHistory, false, oldOptions);
}
private boolean doReload(ReplayableHistory history, boolean echo, Options oldOptions) {
if (oldOptions != null) {
try {
resetState();
} catch (IllegalStateException ex) {
currentNameSpace = mainNamespace; // back out of start-up (messages)
errormsg("jshell.err.restart.failed", ex.getMessage());
// attempt recovery to previous option settings
options = oldOptions;
resetState();
}
} else {
resetState();
}
if (history != null) {
run(new ReloadIOContext(history.iterable(),
echo ? cmdout : null));
}
return true;
}
private boolean parseCommandLineLikeFlags(String rawargs, OptionParserBase ap) {
String[] args = Arrays.stream(rawargs.split("\\s+"))
.filter(s -> !s.isEmpty())
.toArray(String[]::new);
Options opts = ap.parse(args);
if (opts == null) {
return false;
}
if (!ap.nonOptions().isEmpty()) {
errormsg("jshell.err.unexpected.at.end", ap.nonOptions(), rawargs);
return false;
}
options = options.override(opts);
return true;
}
private boolean cmdSave(String rawargs) {
// The filename to save to is the last argument, extract it
String[] args = rawargs.split("\\s");
String filename = args[args.length - 1];
if (filename.isEmpty()) {
errormsg("jshell.err.file.filename", "/save");
return false;
}
// All the non-filename arguments are the specifier of what to save
String srcSpec = Arrays.stream(args, 0, args.length - 1)
.collect(Collectors.joining("\n"));
// From the what to save specifier, compute the snippets (as a stream)
ArgTokenizer at = new ArgTokenizer("/save", srcSpec);
at.allowedOptions("-all", "-start", "-history");
Stream<Snippet> snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at);
if (snippetStream == null) {
// error occurred, already reported
return false;
}
try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
Charset.defaultCharset(),
CREATE, TRUNCATE_EXISTING, WRITE)) {
if (at.hasOption("-history")) {
// they want history (commands and snippets), ignore the snippet stream
for (String s : input.history(true)) {
writer.write(s);
writer.write("\n");
}
} else {
// write the snippet stream to the file
writer.write(snippetStream
.map(Snippet::source)
.collect(Collectors.joining("\n")));
}
} catch (FileNotFoundException e) {
errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
return false;
} catch (Exception e) {
errormsg("jshell.err.file.exception", "/save", filename, e);
return false;
}
return true;
}
private boolean cmdVars(String arg) {
Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets,
this::isActive, arg, "/vars");
if (stream == null) {
return false;
}
stream.forEachOrdered(vk ->
{
String val = state.status(vk) == Status.VALID
? feedback.truncateVarValue(state.varValue(vk))
: getResourceString("jshell.msg.vars.not.active");
hard(" %s %s = %s", vk.typeName(), vk.name(), val);
});
return true;
}
private boolean cmdMethods(String arg) {
Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets,
this::isActive, arg, "/methods");
if (stream == null) {
return false;
}
stream.forEachOrdered(meth -> {
String sig = meth.signature();
int i = sig.lastIndexOf(")") + 1;
if (i <= 0) {
hard(" %s", meth.name());
} else {
hard(" %s %s%s", sig.substring(i), meth.name(), sig.substring(0, i));
}
printSnippetStatus(meth, true);
});
return true;
}
private boolean cmdTypes(String arg) {
Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets,
this::isActive, arg, "/types");
if (stream == null) {
return false;
}
stream.forEachOrdered(ck
-> {
String kind;
switch (ck.subKind()) {
case INTERFACE_SUBKIND:
kind = "interface";
break;
case CLASS_SUBKIND:
kind = "class";
break;
case ENUM_SUBKIND:
kind = "enum";
break;
case ANNOTATION_TYPE_SUBKIND:
kind = "@interface";
break;
case RECORD_SUBKIND:
kind = "record";
break;
default:
assert false : "Wrong kind" + ck.subKind();
kind = "class";
break;
}
hard(" %s %s", kind, ck.name());
printSnippetStatus(ck, true);
});
return true;
}
private boolean cmdImports() {
state.imports().forEach(ik -> {
hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname());
});
return true;
}
private boolean cmdUseHistoryEntry(int index) {
List<Snippet> keys = state.snippets().collect(toList());
if (index < 0)
index += keys.size();
else
index--;
if (index >= 0 && index < keys.size()) {
rerunSnippet(keys.get(index));
} else {
errormsg("jshell.err.out.of.range");
return false;
}
return true;
}
boolean checkOptionsAndRemainingInput(ArgTokenizer at) {
String junk = at.remainder();
if (!junk.isEmpty()) {
errormsg("jshell.err.unexpected.at.end", junk, at.whole());
return false;
} else {
String bad = at.badOptions();
if (!bad.isEmpty()) {
errormsg("jshell.err.unknown.option", bad, at.whole());
return false;
}
}
return true;
}
/**
* Handle snippet reevaluation commands: {@code /<id>}. These commands are a
* sequence of ids and id ranges (names are permitted, though not in the
* first position. Support for names is purposely not documented).
*
* @param rawargs the whole command including arguments
*/
private void rerunHistoryEntriesById(String rawargs) {
ArgTokenizer at = new ArgTokenizer("/<id>", rawargs.trim().substring(1));
at.allowedOptions();
Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, sn -> true, at);
if (stream != null) {
// successfully parsed, rerun snippets
stream.forEach(sn -> rerunSnippet(sn));
}
}
private void rerunSnippet(Snippet snippet) {
String source = snippet.source();
cmdout.printf("%s\n", source);
input.replaceLastHistoryEntry(source);
processSourceCatchingReset(source);
}
/**
* Filter diagnostics for only errors (no warnings, ...)
* @param diagnostics input list
* @return filtered list
*/
List<Diag> errorsOnly(List<Diag> diagnostics) {
return diagnostics.stream()
.filter(Diag::isError)
.collect(toList());
}
/**
* Print out a snippet exception.
*
* @param exception the throwable to print
* @return true on fatal exception
*/
private boolean displayException(Throwable exception) {
Throwable rootCause = exception;
while (rootCause instanceof EvalException) {
rootCause = rootCause.getCause();
}
if (rootCause != exception && rootCause instanceof UnresolvedReferenceException) {
// An unresolved reference caused a chained exception, just show the unresolved
return displayException(rootCause, null);
} else {
return displayException(exception, null);
}
}
//where
private boolean displayException(Throwable exception, StackTraceElement[] caused) {
if (exception instanceof EvalException) {
// User exception
return displayEvalException((EvalException) exception, caused);
} else if (exception instanceof UnresolvedReferenceException) {
// Reference to an undefined snippet
return displayUnresolvedException((UnresolvedReferenceException) exception);
} else {
// Should never occur
error("Unexpected execution exception: %s", exception);
return true;
}
}
//where
private boolean displayUnresolvedException(UnresolvedReferenceException ex) {
// Display the resolution issue
printSnippetStatus(ex.getSnippet(), false);
return false;
}
//where
private boolean displayEvalException(EvalException ex, StackTraceElement[] caused) {
// The message for the user exception is configured based on the
// existance of an exception message and if this is a recursive
// invocation for a chained exception.
String msg = ex.getMessage();
String key = "jshell.err.exception" +
(caused == null? ".thrown" : ".cause") +
(msg == null? "" : ".message");
errormsg(key, ex.getExceptionClassName(), msg);
// The caused trace is sent to truncate duplicate elements in the cause trace
printStackTrace(ex.getStackTrace(), caused);
JShellException cause = ex.getCause();
if (cause != null) {
// Display the cause (recursively)
displayException(cause, ex.getStackTrace());
}
return true;
}
/**
* Display a list of diagnostics.
*
* @param source the source line with the error/warning
* @param diagnostics the diagnostics to display
*/
private void displayDiagnostics(String source, List<Diag> diagnostics) {
for (Diag d : diagnostics) {
errormsg(d.isError() ? "jshell.msg.error" : "jshell.msg.warning");
List<String> disp = new ArrayList<>();
displayableDiagnostic(source, d, disp);
disp.stream()
.forEach(l -> error("%s", l));
}
}
/**
* Convert a diagnostic into a list of pretty displayable strings with
* source context.
*
* @param source the source line for the error/warning
* @param diag the diagnostic to convert
* @param toDisplay a list that the displayable strings are added to
*/
private void displayableDiagnostic(String source, Diag diag, List<String> toDisplay) {
for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize
if (!line.trim().startsWith("location:")) {
toDisplay.add(line);
}
}
int pstart = (int) diag.getStartPosition();
int pend = (int) diag.getEndPosition();
if (pstart < 0 || pend < 0) {
pstart = 0;
pend = source.length();
}
Matcher m = LINEBREAK.matcher(source);
int pstartl = 0;
int pendl = -2;
while (m.find(pstartl)) {
pendl = m.start();
if (pendl >= pstart) {
break;
} else {
pstartl = m.end();
}
}
if (pendl < pstartl) {
pendl = source.length();
}
toDisplay.add(source.substring(pstartl, pendl));
StringBuilder sb = new StringBuilder();
int start = pstart - pstartl;
for (int i = 0; i < start; ++i) {
sb.append(' ');
}
sb.append('^');
boolean multiline = pend > pendl;
int end = (multiline ? pendl : pend) - pstartl - 1;
if (end > start) {
for (int i = start + 1; i < end; ++i) {
sb.append('-');
}
if (multiline) {
sb.append("-...");
} else {
sb.append('^');
}
}
toDisplay.add(sb.toString());
debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
debug("Code: %s", diag.getCode());
debug("Pos: %d (%d - %d)", diag.getPosition(),
diag.getStartPosition(), diag.getEndPosition());
}
/**
* Process a source snippet.
*
* @param source the input source
* @return true if the snippet succeeded
*/
boolean processSource(String source) {
debug("Compiling: %s", source);
boolean failed = false;
boolean isActive = false;
List<SnippetEvent> events = state.eval(source);
for (SnippetEvent e : events) {
// Report the event, recording failure
failed |= handleEvent(e);
// If any main snippet is active, this should be replayable
// also ignore var value queries
isActive |= e.causeSnippet() == null &&
e.status().isActive() &&
e.snippet().subKind() != VAR_VALUE_SUBKIND;
}
// If this is an active snippet and it didn't cause the backend to die,
// add it to the replayable history
if (isActive && live) {
addToReplayHistory(source);
}
return !failed;
}
// Handle incoming snippet events -- return true on failure
private boolean handleEvent(SnippetEvent ste) {
Snippet sn = ste.snippet();
if (sn == null) {
debug("Event with null key: %s", ste);
return false;
}
List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
String source = sn.source();
if (ste.causeSnippet() == null) {
// main event
displayDiagnostics(source, diagnostics);
if (ste.status() != Status.REJECTED) {
if (ste.exception() != null) {
if (displayException(ste.exception())) {
return true;
}
} else {
new DisplayEvent(ste, FormatWhen.PRIMARY, ste.value(), diagnostics)
.displayDeclarationAndValue();
}
} else {
if (diagnostics.isEmpty()) {
errormsg("jshell.err.failed");
}
return true;
}
} else {
// Update
if (sn instanceof DeclarationSnippet) {
List<Diag> other = errorsOnly(diagnostics);
// display update information
new DisplayEvent(ste, FormatWhen.UPDATE, ste.value(), other)
/**代码未完, 请加载全部代码(NowJava.com).**/