| /* |
| * Copyright (c) 2014, 2016, 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.BufferedWriter; |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.nio.charset.Charset; |
| import java.nio.file.AccessDeniedException; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Files; |
| import java.nio.file.NoSuchFileException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| 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.MethodSnippet; |
| import jdk.jshell.PersistentSnippet; |
| import jdk.jshell.Snippet; |
| import jdk.jshell.Snippet.Status; |
| import jdk.jshell.Snippet.SubKind; |
| import jdk.jshell.SnippetEvent; |
| import jdk.jshell.SourceCodeAnalysis; |
| import jdk.jshell.SourceCodeAnalysis.CompletionInfo; |
| 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.MissingResourceException; |
| import java.util.Optional; |
| import java.util.ResourceBundle; |
| import java.util.Spliterators; |
| import java.util.function.Function; |
| import java.util.function.Supplier; |
| import jdk.internal.jshell.tool.Feedback.FormatAction; |
| import jdk.internal.jshell.tool.Feedback.FormatCase; |
| import jdk.internal.jshell.tool.Feedback.FormatResolve; |
| import jdk.internal.jshell.tool.Feedback.FormatWhen; |
| import static java.util.stream.Collectors.toList; |
| import static jdk.jshell.Snippet.Kind.METHOD; |
| import static java.util.stream.Collectors.toMap; |
| import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; |
| |
| /** |
| * Command line REPL tool for Java using the JShell API. |
| * @author Robert Field |
| */ |
| public class JShellTool { |
| |
| private static final String LINE_SEP = System.getProperty("line.separator"); |
| private static final Pattern LINEBREAK = Pattern.compile("\\R"); |
| private static final Pattern HISTORY_ALL_START_FILENAME = Pattern.compile( |
| "((?<cmd>(all|history|start))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)"); |
| private static final String RECORD_SEPARATOR = "\u241E"; |
| |
| final InputStream cmdin; |
| final PrintStream cmdout; |
| final PrintStream cmderr; |
| final PrintStream console; |
| final InputStream userin; |
| final PrintStream userout; |
| final PrintStream usererr; |
| |
| final Feedback feedback = new Feedback(); |
| |
| /** |
| * The constructor for the tool (used by tool launch via main and by test |
| * harnesses to capture ins and outs. |
| * @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 (not yet functional) |
| * @param userout code execution output -- System.out.printf("hi") |
| * @param usererr code execution error stream -- System.err.printf("Oops") |
| */ |
| public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, |
| PrintStream console, |
| InputStream userin, PrintStream userout, PrintStream usererr) { |
| this.cmdin = cmdin; |
| this.cmdout = cmdout; |
| this.cmderr = cmderr; |
| this.console = console; |
| this.userin = userin; |
| this.userout = userout; |
| this.usererr = usererr; |
| initializeFeedbackModes(); |
| } |
| |
| /** |
| * Create the default set of feedback modes |
| */ |
| final void initializeFeedbackModes() { |
| // Initialize normal feedback mode |
| cmdSet("newmode normal command"); |
| cmdSet("prompt normal '\n-> ' '>> '"); |
| cmdSet("field normal pre '| '"); |
| cmdSet("field normal post '%n'"); |
| cmdSet("field normal errorpre '| '"); |
| cmdSet("field normal errorpost '%n'"); |
| cmdSet("field normal action 'Added' added-primary"); |
| cmdSet("field normal action 'Modified' modified-primary"); |
| cmdSet("field normal action 'Replaced' replaced-primary"); |
| cmdSet("field normal action 'Overwrote' overwrote-primary"); |
| cmdSet("field normal action 'Dropped' dropped-primary"); |
| cmdSet("field normal action 'Rejected' rejected-primary"); |
| cmdSet("field normal action ' Update added' added-update"); |
| cmdSet("field normal action ' Update modified' modified-update"); |
| cmdSet("field normal action ' Update replaced' replaced-update"); |
| cmdSet("field normal action ' Update overwrote' overwrote-update"); |
| cmdSet("field normal action ' Update dropped' dropped-update"); |
| cmdSet("field normal action ' Update rejected' rejected-update"); |
| cmdSet("field normal resolve '' ok-*"); |
| cmdSet("field normal resolve ', however, it cannot be invoked until%s is declared' defined-primary"); |
| cmdSet("field normal resolve ', however, it cannot be referenced until%s is declared' notdefined-primary"); |
| cmdSet("field normal resolve ' which cannot be invoked until%s is declared' defined-update"); |
| cmdSet("field normal resolve ' which cannot be referenced until%s is declared' notdefined-update"); |
| cmdSet("field normal name '%s'"); |
| cmdSet("field normal type '%s'"); |
| cmdSet("field normal result '%s'"); |
| |
| cmdSet("format normal '' *-*-*"); |
| |
| cmdSet("format normal '{pre}{action} class {name}{resolve}{post}' class"); |
| cmdSet("format normal '{pre}{action} interface {name}{resolve}{post}' interface"); |
| cmdSet("format normal '{pre}{action} enum {name}{resolve}{post}' enum"); |
| cmdSet("format normal '{pre}{action} annotation interface {name}{resolve}{post}' annotation"); |
| |
| cmdSet("format normal '{pre}{action} method {name}({type}){resolve}{post}' method"); |
| |
| cmdSet("format normal '{pre}{action} variable {name} of type {type}{resolve}{post}' vardecl"); |
| cmdSet("format normal '{pre}{action} variable {name} of type {type} with initial value {result}{resolve}{post}' varinit"); |
| cmdSet("format normal '{pre}{action} variable {name}{resolve}{post}' vardeclrecoverable"); |
| cmdSet("format normal '{pre}{action} variable {name}, reset to null{post}' varreset-*-update"); |
| |
| cmdSet("format normal '{pre}Expression value is: {result}{post}" + |
| "{pre} assigned to temporary variable {name} of type {type}{post}' expression"); |
| cmdSet("format normal '{pre}Variable {name} of type {type} has value {result}{post}' varvalue"); |
| cmdSet("format normal '{pre}Variable {name} has been assigned the value {result}{post}' assignment"); |
| |
| cmdSet("feedback normal"); |
| |
| // Initialize off feedback mode |
| cmdSet("newmode off quiet"); |
| cmdSet("prompt off '-> ' '>> '"); |
| cmdSet("field off pre '| '"); |
| cmdSet("field off post '%n'"); |
| cmdSet("field off errorpre '| '"); |
| cmdSet("field off errorpost '%n'"); |
| cmdSet("format off '' *-*-*"); |
| } |
| |
| private IOContext input = null; |
| private boolean regenerateOnDeath = true; |
| private boolean live = false; |
| |
| SourceCodeAnalysis analysis; |
| JShell state = null; |
| Subscription shutdownSubscription = null; |
| |
| private boolean debug = false; |
| private boolean displayPrompt = true; |
| public boolean testPrompt = false; |
| private String cmdlineClasspath = null; |
| private String cmdlineStartup = null; |
| private String[] editor = null; |
| |
| // Commands and snippets which should be replayed |
| private List<String> replayableHistory; |
| private List<String> replayableHistoryPrevious; |
| |
| static final Preferences PREFS = Preferences.userRoot().node("tool/JShell"); |
| |
| static final String STARTUP_KEY = "STARTUP"; |
| static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; |
| |
| static final String DEFAULT_STARTUP = |
| "\n" + |
| "import java.util.*;\n" + |
| "import java.io.*;\n" + |
| "import java.math.*;\n" + |
| "import java.net.*;\n" + |
| "import java.util.concurrent.*;\n" + |
| "import java.util.prefs.*;\n" + |
| "import java.util.regex.*;\n" + |
| "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; |
| |
| // 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; |
| |
| /** |
| * 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); |
| } |
| } |
| |
| /** |
| * Base output for command output -- no pre- or post-fix |
| * |
| * @param printf format |
| * @param printf args |
| */ |
| void rawout(String format, Object... args) { |
| cmdout.printf(format, args); |
| } |
| |
| /** |
| * Must show command output |
| * |
| * @param format printf format |
| * @param args printf args |
| */ |
| void hard(String format, Object... args) { |
| rawout(feedback.getPre() + format + feedback.getPost(), args); |
| } |
| |
| /** |
| * Error command output |
| * |
| * @param format printf format |
| * @param args printf args |
| */ |
| void error(String format, Object... args) { |
| rawout(feedback.getErrorPre() + format + feedback.getErrorPost(), args); |
| } |
| |
| /** |
| * Optional output |
| * |
| * @param format printf format |
| * @param args printf args |
| */ |
| void fluff(String format, Object... args) { |
| if (feedback.shouldDisplayCommandFluff() && interactive()) { |
| hard(format, args); |
| } |
| } |
| |
| /** |
| * Optional output -- with embedded per- and post-fix |
| * |
| * @param format printf format |
| * @param args printf args |
| */ |
| void fluffRaw(String format, Object... args) { |
| if (feedback.shouldDisplayCommandFluff() && interactive()) { |
| rawout(format, 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, |
| () -> new LinkedHashMap<>())); |
| int aLen = 0; |
| for (String av : a2b.keySet()) { |
| aLen = Math.max(aLen, av.length()); |
| } |
| String format = " %-" + aLen + "s -- %s"; |
| String indentedNewLine = LINE_SEP + feedback.getPre() |
| + String.format(" %-" + (aLen + 4) + "s", ""); |
| for (Entry<String, String> e : a2b.entrySet()) { |
| hard(format, e.getKey(), e.getValue().replaceAll("\n", indentedNewLine)); |
| } |
| } |
| |
| /** |
| * User custom feedback mode only |
| * |
| * @param fcase Event to report |
| * @param update Is this an update (rather than primary) |
| * @param fa Action |
| * @param fr Resolution status |
| * @param name Name string |
| * @param type Type string or null |
| * @param result Result value or null |
| * @param unresolved The unresolved symbols |
| */ |
| void custom(FormatCase fcase, boolean update, FormatAction fa, FormatResolve fr, |
| String name, String type, String unresolved, String result) { |
| String format = feedback.getFormat(fcase, |
| (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY), fa, fr, |
| name != null, type != null, result != null); |
| fluffRaw(format, unresolved, name, type, result); |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| /** |
| * Normal start entry point |
| * @param args |
| * @throws Exception |
| */ |
| public static void main(String[] args) throws Exception { |
| new JShellTool(System.in, System.out, System.err, System.out, |
| new ByteArrayInputStream(new byte[0]), System.out, System.err) |
| .start(args); |
| } |
| |
| public void start(String[] args) throws Exception { |
| List<String> loadList = processCommandArgs(args); |
| if (loadList == null) { |
| // Abort |
| return; |
| } |
| try (IOContext in = new ConsoleIOContext(this, cmdin, console)) { |
| start(in, loadList); |
| } |
| } |
| |
| private void start(IOContext in, List<String> loadList) { |
| resetState(); // Initialize |
| |
| // Read replay history from last jshell session into previous history |
| String prevReplay = PREFS.get(REPLAY_RESTORE_KEY, null); |
| if (prevReplay != null) { |
| replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR)); |
| } |
| |
| for (String loadFile : loadList) { |
| cmdOpen(loadFile); |
| } |
| |
| if (regenerateOnDeath) { |
| hard("Welcome to JShell -- Version %s", version()); |
| hard("Type /help for help"); |
| } |
| |
| try { |
| while (regenerateOnDeath) { |
| if (!live) { |
| resetState(); |
| } |
| run(in); |
| } |
| } finally { |
| closeState(); |
| } |
| } |
| |
| /** |
| * Process the command line arguments. |
| * Set options. |
| * @param args the command line arguments |
| * @return the list of files to be loaded |
| */ |
| private List<String> processCommandArgs(String[] args) { |
| List<String> loadList = new ArrayList<>(); |
| Iterator<String> ai = Arrays.asList(args).iterator(); |
| while (ai.hasNext()) { |
| String arg = ai.next(); |
| if (arg.startsWith("-")) { |
| switch (arg) { |
| case "-classpath": |
| case "-cp": |
| if (cmdlineClasspath != null) { |
| cmderr.printf("Conflicting -classpath option.\n"); |
| return null; |
| } |
| if (ai.hasNext()) { |
| cmdlineClasspath = ai.next(); |
| } else { |
| cmderr.printf("Argument to -classpath missing.\n"); |
| return null; |
| } |
| break; |
| case "-help": |
| printUsage(); |
| return null; |
| case "-version": |
| cmdout.printf("jshell %s\n", version()); |
| return null; |
| case "-fullversion": |
| cmdout.printf("jshell %s\n", fullVersion()); |
| return null; |
| case "-startup": |
| if (cmdlineStartup != null) { |
| cmderr.printf("Conflicting -startup or -nostartup option.\n"); |
| return null; |
| } |
| if (ai.hasNext()) { |
| String filename = ai.next(); |
| try { |
| byte[] encoded = Files.readAllBytes(Paths.get(filename)); |
| cmdlineStartup = new String(encoded); |
| } catch (AccessDeniedException e) { |
| hard("File '%s' for start-up is not accessible.", filename); |
| } catch (NoSuchFileException e) { |
| hard("File '%s' for start-up is not found.", filename); |
| } catch (Exception e) { |
| hard("Exception while reading start-up file: %s", e); |
| } |
| } else { |
| cmderr.printf("Argument to -startup missing.\n"); |
| return null; |
| } |
| break; |
| case "-nostartup": |
| if (cmdlineStartup != null && !cmdlineStartup.isEmpty()) { |
| cmderr.printf("Conflicting -startup option.\n"); |
| return null; |
| } |
| cmdlineStartup = ""; |
| break; |
| default: |
| cmderr.printf("Unknown option: %s\n", arg); |
| printUsage(); |
| return null; |
| } |
| } else { |
| loadList.add(arg); |
| } |
| } |
| return loadList; |
| } |
| |
| private void printUsage() { |
| rawout("Usage: jshell <options> <load files>\n"); |
| rawout("where possible options include:\n"); |
| rawout(" -classpath <path> Specify where to find user class files\n"); |
| rawout(" -cp <path> Specify where to find user class files\n"); |
| rawout(" -startup <file> One run replacement for the start-up definitions\n"); |
| rawout(" -nostartup Do not run the start-up definitions\n"); |
| rawout(" -help Print a synopsis of standard options\n"); |
| rawout(" -version Version information\n"); |
| } |
| |
| 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 = new ArrayList<>(); |
| |
| state = 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)) |
| .build(); |
| analysis = state.sourceCodeAnalysis(); |
| shutdownSubscription = state.onShutdown((JShell deadState) -> { |
| if (deadState == state) { |
| hard("State engine terminated."); |
| hard("Restore definitions with: /reload restore"); |
| live = false; |
| } |
| }); |
| live = true; |
| |
| if (cmdlineClasspath != null) { |
| state.addToClasspath(cmdlineClasspath); |
| } |
| |
| String start; |
| if (cmdlineStartup == null) { |
| start = PREFS.get(STARTUP_KEY, "<nada>"); |
| if (start.equals("<nada>")) { |
| start = DEFAULT_STARTUP; |
| PREFS.put(STARTUP_KEY, DEFAULT_STARTUP); |
| } |
| } else { |
| start = cmdlineStartup; |
| } |
| try (IOContext suin = new FileScannerIOContext(new StringReader(start))) { |
| run(suin); |
| } catch (Exception ex) { |
| hard("Unexpected exception reading start-up: %s\n", ex); |
| } |
| currentNameSpace = mainNamespace; |
| } |
| |
| private void closeState() { |
| live = false; |
| JShell oldState = state; |
| if (oldState != 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 { |
| String incomplete = ""; |
| while (live) { |
| String prompt; |
| if (displayPrompt) { |
| prompt = testPrompt |
| ? incomplete.isEmpty() |
| ? "\u0005" //ENQ |
| : "\u0006" //ACK |
| : incomplete.isEmpty() |
| ? feedback.getPrompt(currentNameSpace.tidNext()) |
| : feedback.getContinuationPrompt(currentNameSpace.tidNext()) |
| ; |
| } else { |
| prompt = ""; |
| } |
| String raw; |
| try { |
| raw = in.readLine(prompt, incomplete); |
| } catch (InputInterruptedException ex) { |
| //input interrupted - clearing current state |
| incomplete = ""; |
| continue; |
| } |
| if (raw == null) { |
| //EOF |
| if (in.interactiveOutput()) { |
| // End after user ctrl-D |
| regenerateOnDeath = false; |
| } |
| break; |
| } |
| String trimmed = trimEnd(raw); |
| if (!trimmed.isEmpty()) { |
| String line = incomplete + trimmed; |
| |
| // No commands in the middle of unprocessed source |
| if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) { |
| processCommand(line.trim()); |
| } else { |
| incomplete = processSourceCatchingReset(line); |
| } |
| } |
| } |
| } catch (IOException ex) { |
| hard("Unexpected exception: %s\n", ex); |
| } finally { |
| input = oldInput; |
| } |
| } |
| |
| private void addToReplayHistory(String s) { |
| if (currentNameSpace == mainNamespace) { |
| replayableHistory.add(s); |
| } |
| } |
| |
| private String processSourceCatchingReset(String src) { |
| try { |
| input.beforeUserCode(); |
| return processSource(src); |
| } catch (IllegalStateException ex) { |
| hard("Resetting..."); |
| live = false; // Make double sure |
| return ""; |
| } finally { |
| input.afterUserCode(); |
| } |
| } |
| |
| private void processCommand(String cmd) { |
| if (cmd.startsWith("/-")) { |
| try { |
| //handle "/-[number]" |
| cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1))); |
| return ; |
| } catch (NumberFormatException ex) { |
| //ignore |
| } |
| } |
| String arg = ""; |
| int idx = cmd.indexOf(' '); |
| if (idx > 0) { |
| arg = cmd.substring(idx + 1).trim(); |
| cmd = cmd.substring(0, idx); |
| } |
| Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand); |
| if (candidates.length == 0) { |
| if (!rerunHistoryEntryById(cmd.substring(1))) { |
| error("No such command or snippet id: %s", cmd); |
| fluff("Type /help for help."); |
| } |
| } else if (candidates.length == 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()); |
| } |
| } else { |
| error("Command: %s is ambiguous: %s", cmd, Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); |
| fluff("Type /help for help."); |
| } |
| } |
| |
| 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(size -> new Command[size]); |
| } |
| |
| private 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 params; |
| public final String description; |
| public final String help; |
| public final Function<String,Boolean> run; |
| public final CompletionProvider completions; |
| public final CommandKind kind; |
| |
| // NORMAL Commands |
| public Command(String command, String params, String description, String help, |
| Function<String,Boolean> run, CompletionProvider completions) { |
| this(command, params, description, help, |
| run, completions, CommandKind.NORMAL); |
| } |
| |
| // Documentation pseudo-commands |
| public Command(String command, String description, String help, |
| CommandKind kind) { |
| this(command, null, description, help, |
| arg -> { throw new IllegalStateException(); }, |
| EMPTY_COMPLETION_PROVIDER, |
| kind); |
| } |
| |
| public Command(String command, String params, String description, String help, |
| Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { |
| this.command = command; |
| this.params = params; |
| this.description = description; |
| this.help = help; |
| 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; |
| } |
| |
| @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 Suggestion(alternative, false)); |
| } |
| } |
| |
| anchor[0] = 0; |
| |
| return result; |
| } |
| |
| } |
| |
| private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); |
| private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("all ", "start ", "history "); |
| private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("restore", "quiet"); |
| private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); |
| private final Map<String, Command> commands = new LinkedHashMap<>(); |
| private void registerCommand(Command cmd) { |
| commands.put(cmd.command, cmd); |
| } |
| 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 Suggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""), false)) |
| .forEach(result::add); |
| } catch (IOException ex) { |
| //ignore... |
| } |
| if (path.isEmpty()) { |
| StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false) |
| .filter(root -> accept.test(root) && root.toString().startsWith(prefix)) |
| .map(root -> new Suggestion(root.toString(), false)) |
| .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")); |
| } |
| |
| private CompletionProvider editCompletion() { |
| return (prefix, cursor, anchor) -> { |
| anchor[0] = 0; |
| return state.snippets() |
| .stream() |
| .flatMap(k -> (k instanceof DeclarationSnippet) |
| ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name()) |
| : Stream.of(String.valueOf(k.id()))) |
| .filter(k -> k.startsWith(prefix)) |
| .map(k -> new Suggestion(k, false)) |
| .collect(Collectors.toList()); |
| }; |
| } |
| |
| private CompletionProvider editKeywordCompletion() { |
| return (code, cursor, anchor) -> { |
| List<Suggestion> result = new ArrayList<>(); |
| result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); |
| result.addAll(editCompletion().completionSuggestions(code, cursor, anchor)); |
| return result; |
| }; |
| } |
| |
| private static CompletionProvider saveCompletion() { |
| return (code, cursor, anchor) -> { |
| List<Suggestion> result = new ArrayList<>(); |
| int space = code.indexOf(' '); |
| if (space == (-1)) { |
| result.addAll(KEYWORD_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; |
| }; |
| } |
| |
| private static CompletionProvider reloadCompletion() { |
| return (code, cursor, anchor) -> { |
| List<Suggestion> result = new ArrayList<>(); |
| int pastSpace = code.indexOf(' ') + 1; // zero if no space |
| result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor)); |
| anchor[0] += pastSpace; |
| return result; |
| }; |
| } |
| |
| // Table of commands -- with command forms, argument kinds, help message, implementation, ... |
| |
| { |
| registerCommand(new Command("/list", "[all|start|<name or id>]", "list the source you have typed", |
| "Show the source of snippets, prefaced with the snippet id.\n\n" + |
| "/list\n" + |
| " -- List the currently active snippets of code that you typed or read with /open\n" + |
| "/list start\n" + |
| " -- List the automatically evaluated start-up snippets\n" + |
| "/list all\n" + |
| " -- List all snippets including failed, overwritten, dropped, and start-up\n" + |
| "/list <name>\n" + |
| " -- List snippets with the specified name (preference for active snippets)\n" + |
| "/list <id>\n" + |
| " -- List the snippet with the specified snippet id\n", |
| arg -> cmdList(arg), |
| editKeywordCompletion())); |
| registerCommand(new Command("/edit", "<name or id>", "edit a source entry referenced by name or id", |
| "Edit a snippet or snippets of source in an external editor.\n" + |
| "The editor to use is set with /set editor.\n" + |
| "If no editor has been set, a simple editor will be launched.\n\n" + |
| "/edit <name>\n" + |
| " -- Edit the snippet or snippets with the specified name (preference for active snippets)\n" + |
| "/edit <id>\n" + |
| " -- Edit the snippet with the specified snippet id\n" + |
| "/edit\n" + |
| " -- Edit the currently active snippets of code that you typed or read with /open\n", |
| arg -> cmdEdit(arg), |
| editCompletion())); |
| registerCommand(new Command("/drop", "<name or id>", "delete a source entry referenced by name or id", |
| "Drop a snippet -- making it inactive.\n\n" + |
| "/drop <name>\n" + |
| " -- Drop the snippet with the specified name\n" + |
| "/drop <id>\n" + |
| " -- Drop the snippet with the specified snippet id\n", |
| arg -> cmdDrop(arg), |
| editCompletion(), |
| CommandKind.REPLAY)); |
| registerCommand(new Command("/save", "[all|history|start] <file>", "Save snippet source to a file.", |
| "Save the specified snippets and/or commands to the specified file.\n\n" + |
| "/save <file>\n" + |
| " -- Save the source of current active snippets to the file\n" + |
| "/save all <file>\n" + |
| " -- Save the source of all snippets to the file\n" + |
| " Includes source including overwritten, failed, and start-up code\n" + |
| "/save history <file>\n" + |
| " -- Save the sequential history of all commands and snippets entered since jshell was launched\n" + |
| "/save start <file>\n" + |
| " -- Save the default start-up definitions to the file\n", |
| arg -> cmdSave(arg), |
| saveCompletion())); |
| registerCommand(new Command("/open", "<file>", "open a file as source input", |
| "Open a file and read its contents as snippets and commands.\n\n" + |
| "/open <file>\n" + |
| " -- Read the specified file as jshell input.\n", |
| arg -> cmdOpen(arg), |
| FILE_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/vars", null, "list the declared variables and their values", |
| "List the type, name, and value of the current active jshell variables.\n", |
| arg -> cmdVars(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/methods", null, "list the declared methods and their signatures", |
| "List the name, parameter types, and return type of the current active jshell methods.\n", |
| arg -> cmdMethods(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/classes", null, "list the declared classes", |
| "List the current active jshell classes, interfaces, and enums.\n", |
| arg -> cmdClasses(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/imports", null, "list the imported items", |
| "List the current active jshell imports.\n", |
| arg -> cmdImports(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/exit", null, "exit jshell", |
| "Leave the jshell tool. No work is saved.\n" + |
| "Save any work before using this command\n", |
| arg -> cmdExit(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/reset", null, "reset jshell", |
| "Reset the jshell tool code and execution state:\n" + |
| " * All entered code is lost.\n" + |
| " * Start-up code is re-executed.\n" + |
| " * The execution state is restarted.\n" + |
| " * The classpath is cleared.\n" + |
| "Tool settings are maintained, as set with: /set ...\n" + |
| "Save any work before using this command\n", |
| arg -> cmdReset(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/reload", "[restore] [quiet]", "reset and replay relevant history -- current or previous (restore)", |
| "Reset the jshell tool code and execution state then replay each\n" + |
| "jshell valid command and valid snippet in the order they were entered.\n\n" + |
| "/reload\n" + |
| " -- Reset and replay the valid history since jshell was entered, or\n" + |
| " a /reset, or /reload command was executed -- whichever is most\n" + |
| " recent.\n" + |
| "/reload restore\n" + |
| " -- Reset and replay the valid history between the previous and most\n" + |
| " recent time that jshell was entered, or a /reset, or /reload\n" + |
| " command was executed. This can thus be used to restore a previous\n" + |
| " jshell tool sesson.\n" + |
| "/reload [restore] quiet\n" + |
| " -- With the 'quiet' argument the replay is not shown. Errors will display.\n", |
| arg -> cmdReload(arg), |
| reloadCompletion())); |
| registerCommand(new Command("/classpath", "<path>", "add a path to the classpath", |
| "Append a additional path to the classpath.\n", |
| arg -> cmdClasspath(arg), |
| classPathCompletion(), |
| CommandKind.REPLAY)); |
| registerCommand(new Command("/history", null, "history of what you have typed", |
| "Display the history of snippet and command input since this jshell was launched.\n", |
| arg -> cmdHistory(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/debug", null, "toggle debugging of the jshell", |
| "Display debugging information for the jshelll implementation.\n" + |
| "0: Debugging off\n" + |
| "r: Debugging on\n" + |
| "g: General debugging on\n" + |
| "f: File manager debugging on\n" + |
| "c': Completion analysis debugging on\n" + |
| "d': Dependency debugging on\n" + |
| "e': Event debugging on\n", |
| arg -> cmdDebug(arg), |
| EMPTY_COMPLETION_PROVIDER, |
| CommandKind.HIDDEN)); |
| registerCommand(new Command("/help", "[<command>|<subject>]", "get information about jshell", |
| "Display information about jshell.\n" + |
| "/help\n" + |
| " -- List the jshell commands and help subjects.\n" + |
| "/help <command>\n" + |
| " -- Display information about the specified comand. The slash must be included.\n" + |
| " Only the first few letters of the command are needed -- if more than one\n" + |
| " each will be displayed. Example: /help /li\n" + |
| "/help <subject>\n" + |
| " -- Display information about the specified help subject. Example: /help intro\n", |
| arg -> cmdHelp(arg), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/set", "editor|start|feedback|newmode|prompt|format|field ...", "set jshell configuration information", |
| "Set jshell configuration information, including:\n" + |
| "the external editor to use, the start-up definitions to use, a new feedback mode,\n" + |
| "the command prompt, the feedback mode to use, or the format of output.\n" + |
| "\n" + |
| "/set editor <command> <optional-arg>...\n" + |
| " -- Specify the command to launch for the /edit command.\n" + |
| " The <command> is an operating system dependent string.\n" + |
| "\n" + |
| "/set start <file>\n" + |
| " -- The contents of the specified <file> become the default start-up snippets and commands.\n" + |
| "\n" + |
| "/set feedback <mode>\n" + |
| " -- Set the feedback mode describing displayed feedback for entered snippets and commands.\n" + |
| "\n" + |
| "/set newmode <new-mode> [command|quiet [<old-mode>]]\n" + |
| " -- Create a user-defined feedback mode, optionally copying from an existing mode.\n" + |
| "\n" + |
| "/set prompt <mode> \"<prompt>\" \"<continuation-prompt>\"\n" + |
| " -- Set the displayed prompts for a given feedback mode.\n" + |
| "\n" + |
| "/set format <mode> \"<format>\" <selector>...\n" + |
| " -- Configure a feedback mode by setting the format to use in a specified set of cases.\n" + |
| "\n" + |
| "/set field name|type|result|when|action|resolve|pre|post|errorpre|errorpost \"<format>\" <format-case>...\n" + |
| " -- Set the format of a field within the <format-string> of a \"/set format\" command\n" + |
| "\n" + |
| "To get more information about one of these forms, use /help with the form specified.\n" + |
| "For example: /help /set format\n", |
| arg -> cmdSet(arg), |
| new FixedCompletionProvider("format", "field", "feedback", "prompt", "newmode", "start", "editor"))); |
| registerCommand(new Command("/?", "", "get information about jshell", |
| "Display information about jshell (abbreviation for /help).\n" + |
| "/?\n" + |
| " -- Display list of commands and help subjects.\n" + |
| "/? <command>\n" + |
| " -- Display information about the specified comand. The slash must be included.\n" + |
| " Only the first few letters of the command are needed -- if more than one\n" + |
| " match, each will be displayed. Example: /? /li\n" + |
| "/? <subject>\n" + |
| " -- Display information about the specified help subject. Example: /? intro\n", |
| arg -> cmdHelp(arg), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/!", "", "re-run last snippet", |
| "Reevaluate the most recently entered snippet.\n", |
| arg -> cmdUseHistoryEntry(-1), |
| EMPTY_COMPLETION_PROVIDER)); |
| |
| // Documentation pseudo-commands |
| |
| registerCommand(new Command("/<id>", "re-run snippet by id", |
| "", |
| CommandKind.HELP_ONLY)); |
| registerCommand(new Command("/-<n>", "re-run n-th previous snippet", |
| "", |
| CommandKind.HELP_ONLY)); |
| registerCommand(new Command("intro", "An introduction to the jshell tool", |
| "The jshell tool allows you to execute Java code, getting immediate results.\n" + |
| "You can enter a Java definition (variable, method, class, etc), like: int x = 8\n" + |
| "or a Java expression, like: x + x\n" + |
| "or a Java statement or import.\n" + |
| "These little chunks of Java code are called 'snippets'.\n\n" + |
| "There are also jshell commands that allow you to understand and\n" + |
| "control what you are doing, like: /list\n\n" + |
| "For a list of commands: /help", |
| CommandKind.HELP_SUBJECT)); |
| registerCommand(new Command("shortcuts", "Describe shortcuts", |
| "Supported shortcuts include:\n\n" + |
| "<tab> -- After entering the first few letters of a Java identifier,\n" + |
| " a jshell command, or, in some cases, a jshell command argument,\n" + |
| " press the <tab> key to complete the input.\n" + |
| " If there is more than one completion, show possible completions.\n" + |
| "Shift-<tab> -- After the name and open parenthesis of a method or constructor invocation,\n" + |
| " hold the <shift> key and press the <tab> to see a synopsis of all\n" + |
| " matching methods/constructors.\n" + |
| "<fix-shortcut> v -- After a complete expression, press \"<fix-shortcut> v\" to introduce a new variable\n" + |
| " whose type is based on the type of the expression.\n" + |
| " The \"<fix-shortcut>\" is either Alt-F1 or Alt-Enter, depending on the platform.\n" + |
| "<fix-shortcut> i -- After an unresolvable identifier, press \"<fix-shortcut> i\" and jshell will propose\n" + |
| " possible fully qualified names based on the content of the specified classpath.\n" + |
| " The \"<fix-shortcut>\" is either Alt-F1 or Alt-Enter, depending on the platform.\n", |
| CommandKind.HELP_SUBJECT)); |
| } |
| |
| public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) { |
| String prefix = code.substring(0, cursor); |
| int space = prefix.indexOf(' '); |
| Stream<Suggestion> result; |
| |
| if (space == (-1)) { |
| result = commands.values() |
| .stream() |
| .distinct() |
| .filter(cmd -> cmd.kind.shouldSuggestCompletions) |
| .map(cmd -> cmd.command) |
| .filter(key -> key.startsWith(prefix)) |
| .map(key -> new Suggestion(key + " ", false)); |
| anchor[0] = 0; |
| } else { |
| String arg = prefix.substring(space + 1); |
| String cmd = prefix.substring(0, space); |
| Command[] candidates = findCommand(cmd, c -> true); |
| if (candidates.length == 1) { |
| result = candidates[0].completions.completionSuggestions(arg, cursor - space, anchor).stream(); |
| anchor[0] += space + 1; |
| } else { |
| result = Stream.empty(); |
| } |
| } |
| |
| return result.sorted((s1, s2) -> s1.continuation.compareTo(s2.continuation)) |
| .collect(Collectors.toList()); |
| } |
| |
| public String commandDocumentation(String code, int cursor) { |
| code = code.substring(0, cursor); |
| int space = code.indexOf(' '); |
| |
| if (space != (-1)) { |
| String cmd = code.substring(0, space); |
| Command command = commands.get(cmd); |
| if (command != null) { |
| return command.description; |
| } |
| } |
| |
| return null; |
| } |
| |
| // --- Command implementations --- |
| |
| private static final String[] setSub = new String[]{ |
| "format", "field", "feedback", "newmode", "prompt", "editor", "start"}; |
| |
| // The /set command. Currently /set format, /set field and /set feedback. |
| // Other commands will fold here, see: 8148317 |
| final boolean cmdSet(String arg) { |
| ArgTokenizer at = new ArgTokenizer(arg.trim()); |
| String which = setSubCommand(at); |
| if (which == null) { |
| return false; |
| } |
| switch (which) { |
| case "format": |
| return feedback.setFormat(this, at); |
| case "field": |
| return feedback.setField(this, at); |
| case "feedback": |
| return feedback.setFeedback(this, at); |
| case "newmode": |
| return feedback.setNewMode(this, at); |
| case "prompt": |
| return feedback.setPrompt(this, at); |
| case "editor": { |
| String prog = at.next(); |
| if (prog == null) { |
| hard("The '/set editor' command requires a path argument"); |
| return false; |
| } else { |
| List<String> ed = new ArrayList<>(); |
| ed.add(prog); |
| String n; |
| while ((n = at.next()) != null) { |
| ed.add(n); |
| } |
| editor = ed.toArray(new String[ed.size()]); |
| fluff("Editor set to: %s", arg); |
| return true; |
| } |
| } |
| case "start": { |
| String filename = at.next(); |
| if (filename == null) { |
| hard("The '/set start' command requires a filename argument."); |
| } else { |
| try { |
| byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename)); |
| String init = new String(encoded); |
| PREFS.put(STARTUP_KEY, init); |
| } catch (AccessDeniedException e) { |
| hard("File '%s' for /set start is not accessible.", filename); |
| return false; |
| } catch (NoSuchFileException e) { |
| hard("File '%s' for /set start is not found.", filename); |
| return false; |
| } catch (Exception e) { |
| hard("Exception while reading start set file: %s", e); |
| return false; |
| } |
| } |
| return true; |
| } |
| default: |
| hard("Error: Invalid /set argument: %s", which); |
| return false; |
| } |
| } |
| |
| boolean printSetHelp(ArgTokenizer at) { |
| String which = setSubCommand(at); |
| if (which == null) { |
| return false; |
| } |
| switch (which) { |
| case "format": |
| feedback.printFormatHelp(this); |
| return true; |
| case "field": |
| feedback.printFieldHelp(this); |
| return true; |
| case "feedback": |
| feedback.printFeedbackHelp(this); |
| return true; |
| case "newmode": |
| feedback.printNewModeHelp(this); |
| return true; |
| case "prompt": |
| feedback.printPromptHelp(this); |
| return true; |
| case "editor": |
| hard("Specify the command to launch for the /edit command."); |
| hard(""); |
| hard("/set editor <command> <optional-arg>..."); |
| hard(""); |
| hard("The <command> is an operating system dependent string."); |
| hard("The <command> may include space-separated arguments (such as flags) -- <optional-arg>...."); |
| hard("When /edit is used, the temporary file to edit will be appended as the last argument."); |
| return true; |
| case "start": |
| hard("Set the start-up configuration -- a sequence of snippets and commands read at start-up."); |
| hard(""); |
| hard("/set start <file>"); |
| hard(""); |
| hard("The contents of the specified <file> become the default start-up snippets and commands --"); |
| hard("which are run when the jshell tool is started or reset."); |
| return true; |
| default: |
| hard("Error: Invalid /set argument: %s", which); |
| return false; |
| } |
| } |
| |
| String setSubCommand(ArgTokenizer at) { |
| String[] matches = at.next(setSub); |
| if (matches == null) { |
| error("The /set command requires arguments. See: /help /set"); |
| return null; |
| } else if (matches.length == 0) { |
| error("Not a valid argument to /set: %s", at.val()); |
| fluff("/set is followed by one of: %s", Arrays.stream(setSub) |
| .collect(Collectors.joining(", ")) |
| ); |
| return null; |
| } else if (matches.length > 1) { |
| error("Ambiguous argument to /set: %s", at.val()); |
| fluff("Use one of: %s", Arrays.stream(matches) |
| .collect(Collectors.joining(", ")) |
| ); |
| return null; |
| } |
| return matches[0]; |
| } |
| |
| boolean cmdClasspath(String arg) { |
| if (arg.isEmpty()) { |
| hard("/classpath requires a path argument"); |
| return false; |
| } else { |
| state.addToClasspath(toPathResolvingUserHome(arg).toString()); |
| fluff("Path %s added to classpath", arg); |
| return true; |
| } |
| } |
| |
| boolean cmdDebug(String arg) { |
| if (arg.isEmpty()) { |
| debug = !debug; |
| InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0); |
| fluff("Debugging %s", debug ? "on" : "off"); |
| } else { |
| int flags = 0; |
| for (char ch : arg.toCharArray()) { |
| switch (ch) { |
| case '0': |
| flags = 0; |
| debug = false; |
| fluff("Debugging off"); |
| break; |
| case 'r': |
| debug = true; |
| fluff("REPL tool debugging on"); |
| break; |
| case 'g': |
| flags |= InternalDebugControl.DBG_GEN; |
| fluff("General debugging on"); |
| break; |
| case 'f': |
| flags |= InternalDebugControl.DBG_FMGR; |
| fluff("File manager debugging on"); |
| break; |
| case 'c': |
| flags |= InternalDebugControl.DBG_COMPA; |
| fluff("Completion analysis debugging on"); |
| break; |
| case 'd': |
| flags |= InternalDebugControl.DBG_DEP; |
| fluff("Dependency debugging on"); |
| break; |
| case 'e': |
| flags |= InternalDebugControl.DBG_EVNT; |
| fluff("Event debugging on"); |
| break; |
| default: |
| hard("Unknown debugging option: %c", ch); |
| fluff("Use: 0 r g f c d"); |
| return false; |
| } |
| } |
| InternalDebugControl.setDebugFlags(state, flags); |
| } |
| return true; |
| } |
| |
| private boolean cmdExit() { |
| regenerateOnDeath = false; |
| live = false; |
| if (!replayableHistory.isEmpty()) { |
| PREFS.put(REPLAY_RESTORE_KEY, replayableHistory.stream().reduce( |
| (a, b) -> a + RECORD_SEPARATOR + b).get()); |
| } |
| fluff("Goodbye\n"); |
| return true; |
| } |
| |
| boolean cmdHelp(String arg) { |
| ArgTokenizer at = new ArgTokenizer(arg); |
| String subject = at.next(); |
| if (subject != null) { |
| Command[] matches = commands.values().stream() |
| .filter(c -> c.command.startsWith(subject)) |
| .toArray(size -> new Command[size]); |
| at.mark(); |
| String sub = at.next(); |
| if (sub != null && matches.length == 1 && matches[0].command.equals("/set")) { |
| at.rewind(); |
| return printSetHelp(at); |
| } |
| if (matches.length > 0) { |
| for (Command c : matches) { |
| hard(""); |
| hard("%s", c.command); |
| hard(""); |
| hard("%s", c.help.replaceAll("\n", LINE_SEP + feedback.getPre())); |
| } |
| return true; |
| } else { |
| error("No commands or subjects start with the provided argument: %s\n\n", arg); |
| } |
| } |
| hard("Type a Java language expression, statement, or declaration."); |
| hard("Or type one of the following commands:"); |
| hard(""); |
| hardPairs(commands.values().stream() |
| .filter(cmd -> cmd.kind.showInHelp), |
| cmd -> (cmd.params != null) |
| ? cmd.command + " " + cmd.params |
| : cmd.command, |
| cmd -> cmd.description |
| ); |
| hard(""); |
| hard("For more information type '/help' followed by the name of command or a subject."); |
| hard("For example '/help /list' or '/help intro'. Subjects:"); |
| hard(""); |
| hardPairs(commands.values().stream() |
| .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), |
| cmd -> cmd.command, |
| cmd -> cmd.description |
| ); |
| return true; |
| } |
| |
| private boolean cmdHistory() { |
| cmdout.println(); |
| for (String s : input.currentSessionHistory()) { |
| // No number prefix, confusing with snippet ids |
| cmdout.printf("%s\n", s); |
| } |
| return true; |
| } |
| |
| /** |
| * Avoid parameterized varargs possible heap pollution warning. |
| */ |
| private interface SnippetPredicate extends Predicate<Snippet> { } |
| |
| /** |
| * 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 |
| */ |
| private static Stream<Snippet> nonEmptyStream(Supplier<Stream<Snippet>> supplier, |
| SnippetPredicate... filters) { |
| for (SnippetPredicate filt : filters) { |
| Iterator<Snippet> 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 a user argument to a Stream of snippets referenced by that argument |
| * (or lack of argument). |
| * |
| * @param arg The user's argument to the command, maybe be the empty string |
| * @return a Stream of referenced snippets or null if no matches to specific arg |
| */ |
| private Stream<Snippet> argToSnippets(String arg, boolean allowAll) { |
| List<Snippet> snippets = state.snippets(); |
| if (allowAll && arg.equals("all")) { |
| // all snippets including start-up, failed, and overwritten |
| return snippets.stream(); |
| } else if (allowAll && arg.equals("start")) { |
| // start-up snippets |
| return snippets.stream() |
| .filter(this::inStartUp); |
| } else if (arg.isEmpty()) { |
| // Default is all active user snippets |
| return snippets.stream() |
| .filter(this::mainActive); |
| } else { |
| Stream<Snippet> result = |
| nonEmptyStream( |
| () -> snippets.stream(), |
| // 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) |
| ); |
| return result; |
| } |
| } |
| |
| private boolean cmdDrop(String arg) { |
| if (arg.isEmpty()) { |
| hard("In the /drop argument, please specify an import, variable, method, or class to drop."); |
| hard("Specify by id or name. Use /list to see ids. Use /reset to reset all state."); |
| return false; |
| } |
| Stream<Snippet> stream = argToSnippets(arg, false); |
| if (stream == null) { |
| hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
| return false; |
| } |
| List<Snippet> snippets = stream |
| .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet) |
| .collect(toList()); |
| if (snippets.isEmpty()) { |
| hard("The argument did not specify an active import, variable, method, or class to drop."); |
| return false; |
| } |
| if (snippets.size() > 1) { |
| hard("The argument references more than one import, variable, method, or class."); |
| hard("Try again with one of the ids below:"); |
| for (Snippet sn : snippets) { |
| cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
| } |
| return false; |
| } |
| PersistentSnippet psn = (PersistentSnippet) snippets.get(0); |
| state.drop(psn).forEach(this::handleEvent); |
| return true; |
| } |
| |
| private boolean cmdEdit(String arg) { |
| Stream<Snippet> stream = argToSnippets(arg, true); |
| if (stream == null) { |
| hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
| 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: |
| 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 == null) { |
| EditPad.edit(errorHandler, src, saveHandler); |
| } else { |
| ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); |
| } |
| return true; |
| } |
| //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 = processCompleteSource(tsrc); |
| } |
| nextSrcs.add(tsrc); |
| if (an.remaining.isEmpty()) { |
| break; |
| } |
| s = an.remaining; |
| } |
| currSrcs = nextSrcs; |
| } catch (IllegalStateException ex) { |
| hard("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.equals("history")) { |
| return cmdHistory(); |
| } |
| Stream<Snippet> stream = argToSnippets(arg, true); |
| if (stream == null) { |
| // Check if there are any definitions at all |
| if (argToSnippets("", false).iterator().hasNext()) { |
| hard("No definition or id named %s found. Try /list without arguments.", arg); |
| } else { |
| hard("No definition or id named %s found. There are no active definitions.", arg); |
| } |
| 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) { |
| if (filename.isEmpty()) { |
| hard("The /open command requires a filename argument."); |
| return false; |
| } else { |
| try { |
| run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); |
| } catch (FileNotFoundException e) { |
| hard("File '%s' is not found: %s", filename, e.getMessage()); |
| return false; |
| } catch (Exception e) { |
| hard("Exception while reading file: %s", e); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean cmdReset() { |
| live = false; |
| fluff("Resetting state."); |
| return true; |
| } |
| |
| private boolean cmdReload(String arg) { |
| Iterable<String> history = replayableHistory; |
| boolean echo = true; |
| if (arg.length() > 0) { |
| if ("restore".startsWith(arg)) { |
| if (replayableHistoryPrevious == null) { |
| hard("No previous history to restore\n", arg); |
| return false; |
| } |
| history = replayableHistoryPrevious; |
| } else if ("quiet".startsWith(arg)) { |
| echo = false; |
| } else { |
| hard("Invalid argument to reload command: %s\nUse 'restore', 'quiet', or no argument\n", arg); |
| return false; |
| } |
| } |
| fluff("Restarting and restoring %s.", |
| history == replayableHistoryPrevious |
| ? "from previous state" |
| : "state"); |
| resetState(); |
| run(new ReloadIOContext(history, |
| echo? cmdout : null)); |
| return true; |
| } |
| |
| private boolean cmdSave(String arg_filename) { |
| Matcher mat = HISTORY_ALL_START_FILENAME.matcher(arg_filename); |
| if (!mat.find()) { |
| hard("Malformed argument to the /save command: %s", arg_filename); |
| return false; |
| } |
| boolean useHistory = false; |
| String saveAll = ""; |
| boolean saveStart = false; |
| String cmd = mat.group("cmd"); |
| if (cmd != null) switch (cmd) { |
| case "all": |
| saveAll = "all"; |
| break; |
| case "history": |
| useHistory = true; |
| break; |
| case "start": |
| saveStart = true; |
| break; |
| } |
| String filename = mat.group("filename"); |
| if (filename == null ||filename.isEmpty()) { |
| hard("The /save command requires a filename argument."); |
| return false; |
| } |
| try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), |
| Charset.defaultCharset(), |
| CREATE, TRUNCATE_EXISTING, WRITE)) { |
| if (useHistory) { |
| for (String s : input.currentSessionHistory()) { |
| writer.write(s); |
| writer.write("\n"); |
| } |
| } else if (saveStart) { |
| writer.append(DEFAULT_STARTUP); |
| } else { |
| Stream<Snippet> stream = argToSnippets(saveAll, true); |
| if (stream != null) { |
| for (Snippet sn : stream.collect(toList())) { |
| writer.write(sn.source()); |
| writer.write("\n"); |
| } |
| } |
| } |
| } catch (FileNotFoundException e) { |
| hard("File '%s' for save is not accessible: %s", filename, e.getMessage()); |
| return false; |
| } catch (Exception e) { |
| hard("Exception while saving: %s", e); |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean cmdVars() { |
| for (VarSnippet vk : state.variables()) { |
| String val = state.status(vk) == Status.VALID |
| ? state.varValue(vk) |
| : "(not-active)"; |
| hard(" %s %s = %s", vk.typeName(), vk.name(), val); |
| } |
| return true; |
| } |
| |
| private boolean cmdMethods() { |
| for (MethodSnippet mk : state.methods()) { |
| hard(" %s %s", mk.name(), mk.signature()); |
| } |
| return true; |
| } |
| |
| private boolean cmdClasses() { |
| for (TypeDeclSnippet ck : state.types()) { |
| 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; |
| default: |
| assert false : "Wrong kind" + ck.subKind(); |
| kind = "class"; |
| break; |
| } |
| hard(" %s %s", kind, ck.name()); |
| } |
| 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(); |
| if (index < 0) |
| index += keys.size(); |
| else |
| index--; |
| if (index >= 0 && index < keys.size()) { |
| rerunSnippet(keys.get(index)); |
| } else { |
| hard("Cannot find snippet %d", index + 1); |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean rerunHistoryEntryById(String id) { |
| Optional<Snippet> snippet = state.snippets().stream() |
| .filter(s -> s.id().equals(id)) |
| .findFirst(); |
| return snippet.map(s -> { |
| rerunSnippet(s); |
| return true; |
| }).orElse(false); |
| } |
| |
| 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(d -> d.isError()) |
| .collect(toList()); |
| } |
| |
| void printDiagnostics(String source, List<Diag> diagnostics, boolean embed) { |
| String padding = embed? " " : ""; |
| for (Diag diag : diagnostics) { |
| //assert diag.getSource().equals(source); |
| |
| if (!embed) { |
| if (diag.isError()) { |
| hard("Error:"); |
| } else { |
| hard("Warning:"); |
| } |
| } |
| |
| for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize |
| if (!line.trim().startsWith("location:")) { |
| hard("%s%s", padding, line); |
| } |
| } |
| |
| int pstart = (int) diag.getStartPosition(); |
| int pend = (int) diag.getEndPosition(); |
| 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 < pstart) { |
| pendl = source.length(); |
| } |
| fluff("%s%s", padding, 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('^'); |
| } |
| } |
| fluff("%s%s", padding, 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()); |
| } |
| } |
| |
| private String processSource(String srcInput) throws IllegalStateException { |
| while (true) { |
| CompletionInfo an = analysis.analyzeCompletion(srcInput); |
| if (!an.completeness.isComplete) { |
| return an.remaining; |
| } |
| boolean failed = processCompleteSource(an.source); |
| if (failed || an.remaining.isEmpty()) { |
| return ""; |
| } |
| srcInput = an.remaining; |
| } |
| } |
| //where |
| private boolean processCompleteSource(String source) throws IllegalStateException { |
| 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; |
| } |
| |
| 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); |
| String source = sn.source(); |
| if (ste.causeSnippet() == null) { |
| // main event |
| printDiagnostics(source, diagnostics, false); |
| if (ste.status().isActive) { |
| if (ste.exception() != null) { |
| if (ste.exception() instanceof EvalException) { |
| printEvalException((EvalException) ste.exception()); |
| return true; |
| } else if (ste.exception() instanceof UnresolvedReferenceException) { |
| printUnresolved((UnresolvedReferenceException) ste.exception()); |
| } else { |
| hard("Unexpected execution exception: %s", ste.exception()); |
| return true; |
| } |
| } else { |
| displayDeclarationAndValue(ste, false, ste.value()); |
| } |
| } else if (ste.status() == Status.REJECTED) { |
| if (diagnostics.isEmpty()) { |
| hard("Failed."); |
| } |
| return true; |
| } |
| } else if (ste.status() == Status.REJECTED) { |
| //TODO -- I don't believe updates can cause failures any more |
| hard("Caused failure of dependent %s --", ((DeclarationSnippet) sn).name()); |
| printDiagnostics(source, diagnostics, true); |
| } else { |
| // Update |
| if (sn instanceof DeclarationSnippet) { |
| // display update information |
| displayDeclarationAndValue(ste, true, ste.value()); |
| |
| List<Diag> other = errorsOnly(diagnostics); |
| if (other.size() > 0) { |
| printDiagnostics(source, other, true); |
| } |
| } |
| } |
| return false; |
| } |
| |
| @SuppressWarnings("fallthrough") |
| private void displayDeclarationAndValue(SnippetEvent ste, boolean update, String value) { |
| Snippet key = ste.snippet(); |
| FormatAction action; |
| Status status = ste.status(); |
| switch (status) { |
| case VALID: |
| case RECOVERABLE_DEFINED: |
| case RECOVERABLE_NOT_DEFINED: |
| if (ste.previousStatus().isActive) { |
| action = ste.isSignatureChange() |
| ? FormatAction.REPLACED |
| : FormatAction.MODIFIED; |
| } else { |
| action = FormatAction.ADDED; |
| } |
| break; |
| case OVERWRITTEN: |
| action = FormatAction.OVERWROTE; |
| break; |
| case DROPPED: |
| action = FormatAction.DROPPED; |
| break; |
| case REJECTED: |
| action = FormatAction.REJECTED; |
| break; |
| case NONEXISTENT: |
| default: |
| // Should not occur |
| error("Unexpected status: " + ste.previousStatus().toString() + "=>" + status.toString()); |
| return; |
| } |
| FormatResolve resolution; |
| String unresolved; |
| if (key instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { |
| resolution = (status == Status.RECOVERABLE_NOT_DEFINED) |
| ? FormatResolve.NOTDEFINED |
| : FormatResolve.DEFINED; |
| unresolved = unresolved((DeclarationSnippet) key); |
| } else { |
| resolution = FormatResolve.OK; |
| unresolved = ""; |
| } |
| switch (key.subKind()) { |
| case CLASS_SUBKIND: |
| custom(FormatCase.CLASS, update, action, resolution, |
| ((TypeDeclSnippet) key).name(), null, unresolved, null); |
| break; |
| case INTERFACE_SUBKIND: |
| custom(FormatCase.INTERFACE, update, action, resolution, |
| ((TypeDeclSnippet) key).name(), null, unresolved, null); |
| break; |
| case ENUM_SUBKIND: |
| custom(FormatCase.ENUM, update, action, resolution, |
| ((TypeDeclSnippet) key).name(), null, unresolved, null); |
| break; |
| case ANNOTATION_TYPE_SUBKIND: |
| custom(FormatCase.ANNOTATION, update, action, resolution, |
| ((TypeDeclSnippet) key).name(), null, unresolved, null); |
| break; |
| case METHOD_SUBKIND: |
| custom(FormatCase.METHOD, update, action, resolution, |
| ((MethodSnippet) key).name(), ((MethodSnippet) key).parameterTypes(), unresolved, null); |
| break; |
| case VAR_DECLARATION_SUBKIND: |
| case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { |
| VarSnippet vk = (VarSnippet) key; |
| if (status == Status.RECOVERABLE_NOT_DEFINED) { |
| custom(FormatCase.VARDECLRECOVERABLE, update, action, resolution, |
| vk.name(), null, unresolved, null); |
| } else if (update && ste.isSignatureChange()) { |
| custom(FormatCase.VARRESET, update, action, resolution, |
| vk.name(), null, unresolved, value); |
| } else if (key.subKind() == SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND) { |
| custom(FormatCase.VARINIT, update, action, resolution, |
| vk.name(), vk.typeName(), unresolved, value); |
| } else { |
| custom(FormatCase.VARDECL, update, action, resolution, |
| vk.name(), vk.typeName(), unresolved, value); |
| } |
| break; |
| } |
| case TEMP_VAR_EXPRESSION_SUBKIND: { |
| VarSnippet vk = (VarSnippet) key; |
| custom(FormatCase.EXPRESSION, update, action, resolution, |
| vk.name(), vk.typeName(), null, value); |
| break; |
| } |
| case OTHER_EXPRESSION_SUBKIND: |
| error("Unexpected expression form -- value is: %s", (value)); |
| break; |
| case VAR_VALUE_SUBKIND: { |
| ExpressionSnippet ek = (ExpressionSnippet) key; |
| custom(FormatCase.VARVALUE, update, action, resolution, |
| ek.name(), ek.typeName(), null, value); |
| break; |
| } |
| case ASSIGNMENT_SUBKIND: { |
| ExpressionSnippet ek = (ExpressionSnippet) key; |
| custom(FormatCase.ASSIGNMENT, update, action, resolution, |
| ek.name(), ek.typeName(), null, value); |
| break; |
| } |
| case SINGLE_TYPE_IMPORT_SUBKIND: |
| case TYPE_IMPORT_ON_DEMAND_SUBKIND: |
| case SINGLE_STATIC_IMPORT_SUBKIND: |
| case STATIC_IMPORT_ON_DEMAND_SUBKIND: |
| custom(FormatCase.IMPORT, update, action, resolution, |
| ((ImportSnippet) key).name(), null, null, null); |
| break; |
| case STATEMENT_SUBKIND: |
| custom(FormatCase.STATEMENT, update, action, resolution, |
| null, null, null, null); |
| break; |
| } |
| } |
| //where |
| void printStackTrace(StackTraceElement[] stes) { |
| for (StackTraceElement ste : stes) { |
| StringBuilder sb = new StringBuilder(); |
| String cn = ste.getClassName(); |
| if (!cn.isEmpty()) { |
| int dot = cn.lastIndexOf('.'); |
| if (dot > 0) { |
| sb.append(cn.substring(dot + 1)); |
| } else { |
| sb.append(cn); |
| } |
| sb.append("."); |
| } |
| if (!ste.getMethodName().isEmpty()) { |
| sb.append(ste.getMethodName()); |
| sb.append(" "); |
| } |
| String fileName = ste.getFileName(); |
| int lineNumber = ste.getLineNumber(); |
| String loc = ste.isNativeMethod() |
| ? "Native Method" |
| : fileName == null |
| ? "Unknown Source" |
| : lineNumber >= 0 |
| ? fileName + ":" + lineNumber |
| : fileName; |
| hard(" at %s(%s)", sb, loc); |
| |
| } |
| } |
| //where |
| void printUnresolved(UnresolvedReferenceException ex) { |
| DeclarationSnippet corralled = ex.getSnippet(); |
| List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled)); |
| StringBuilder sb = new StringBuilder(); |
| if (otherErrors.size() > 0) { |
| if (state.unresolvedDependencies(corralled).size() > 0) { |
| sb.append(" and"); |
| } |
| if (otherErrors.size() == 1) { |
| sb.append(" this error is addressed --"); |
| } else { |
| sb.append(" these errors are addressed --"); |
| } |
| } else { |
| sb.append("."); |
| } |
| |
| String format = corralled.kind() == METHOD |
| ? "Attempted to call %s which cannot be invoked until%s" |
| : "Attempted to use %s which cannot be accessed until%s"; |
| hard(format, corralled.name(), |
| unresolved(corralled), sb.toString()); |
| if (otherErrors.size() > 0) { |
| printDiagnostics(corralled.source(), otherErrors, true); |
| } |
| } |
| //where |
| void printEvalException(EvalException ex) { |
| if (ex.getMessage() == null) { |
| hard("%s thrown", ex.getExceptionClassName()); |
| } else { |
| hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage()); |
| } |
| printStackTrace(ex.getStackTrace()); |
| } |
| //where |
| String unresolved(DeclarationSnippet key) { |
| List<String> unr = state.unresolvedDependencies(key); |
| StringBuilder sb = new StringBuilder(); |
| int fromLast = unr.size(); |
| if (fromLast > 0) { |
| sb.append(" "); |
| } |
| for (String u : unr) { |
| --fromLast; |
| sb.append(u); |
| if (fromLast == 0) { |
| // No suffix |
| } else if (fromLast == 1) { |
| sb.append(", and "); |
| } else { |
| sb.append(", "); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** The current version number as a string. |
| */ |
| static String version() { |
| return version("release"); // mm.nn.oo[-milestone] |
| } |
| |
| /** The current full version number as a string. |
| */ |
| static String fullVersion() { |
| return version("full"); // mm.mm.oo[-milestone]-build |
| } |
| |
| private static final String versionRBName = "jdk.internal.jshell.tool.resources.version"; |
| private static ResourceBundle versionRB; |
| |
| private static String version(String key) { |
| if (versionRB == null) { |
| try { |
| versionRB = ResourceBundle.getBundle(versionRBName); |
| } catch (MissingResourceException e) { |
| return "(version info not available)"; |
| } |
| } |
| try { |
| return versionRB.getString(key); |
| } |
| catch (MissingResourceException e) { |
| return "(version info not available)"; |
| } |
| } |
| |
| class NameSpace { |
| final String spaceName; |
| final String prefix; |
| private int nextNum; |
| |
| NameSpace(String spaceName, String prefix) { |
| this.spaceName = spaceName; |
| this.prefix = prefix; |
| this.nextNum = 1; |
| } |
| |
| String tid(Snippet sn) { |
| String tid = prefix + nextNum++; |
| mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); |
| return tid; |
| } |
| |
| String tidNext() { |
| return prefix + nextNum; |
| } |
| } |
| |
| static class SnippetInfo { |
| final Snippet snippet; |
| final NameSpace space; |
| final String tid; |
| |
| SnippetInfo(Snippet snippet, NameSpace space, String tid) { |
| this.snippet = snippet; |
| this.space = space; |
| this.tid = tid; |
| } |
| } |
| } |
| |
| abstract class NonInteractiveIOContext extends IOContext { |
| |
| @Override |
| public boolean interactiveOutput() { |
| return false; |
| } |
| |
| @Override |
| public Iterable<String> currentSessionHistory() { |
| return Collections.emptyList(); |
| } |
| |
| @Override |
| public boolean terminalEditorRunning() { |
| return false; |
| } |
| |
| @Override |
| public void suspend() { |
| } |
| |
| @Override |
| public void resume() { |
| } |
| |
| @Override |
| public void beforeUserCode() { |
| } |
| |
| @Override |
| public void afterUserCode() { |
| } |
| |
| @Override |
| public void replaceLastHistoryEntry(String source) { |
| } |
| } |
| |
| class ScannerIOContext extends NonInteractiveIOContext { |
| private final Scanner scannerIn; |
| |
| ScannerIOContext(Scanner scannerIn) { |
| this.scannerIn = scannerIn; |
| } |
| |
| @Override |
| public String readLine(String prompt, String prefix) { |
| if (scannerIn.hasNextLine()) { |
| return scannerIn.nextLine(); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public void close() { |
| scannerIn.close(); |
| } |
| } |
| |
| class FileScannerIOContext extends ScannerIOContext { |
| |
| FileScannerIOContext(String fn) throws FileNotFoundException { |
| this(new FileReader(fn)); |
| } |
| |
| FileScannerIOContext(Reader rdr) throws FileNotFoundException { |
| super(new Scanner(rdr)); |
| } |
| } |
| |
| class ReloadIOContext extends NonInteractiveIOContext { |
| private final Iterator<String> it; |
| private final PrintStream echoStream; |
| |
| ReloadIOContext(Iterable<String> history, PrintStream echoStream) { |
| this.it = history.iterator(); |
| this.echoStream = echoStream; |
| } |
| |
| @Override |
| public String readLine(String prompt, String prefix) { |
| String s = it.hasNext() |
| ? it.next() |
| : null; |
| if (echoStream != null && s != null) { |
| String p = "-: "; |
| String p2 = "\n "; |
| echoStream.printf("%s%s\n", p, s.replace("\n", p2)); |
| } |
| return s; |
| } |
| |
| @Override |
| public void close() { |
| } |
| } |