| /* |
| * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.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.Diag; |
| import jdk.jshell.EvalException; |
| import jdk.jshell.JShell; |
| import jdk.jshell.Snippet; |
| import jdk.jshell.DeclarationSnippet; |
| import jdk.jshell.TypeDeclSnippet; |
| import jdk.jshell.MethodSnippet; |
| import jdk.jshell.PersistentSnippet; |
| import jdk.jshell.VarSnippet; |
| import jdk.jshell.ExpressionSnippet; |
| import jdk.jshell.Snippet.Status; |
| import jdk.jshell.SourceCodeAnalysis; |
| import jdk.jshell.SourceCodeAnalysis.CompletionInfo; |
| import jdk.jshell.SourceCodeAnalysis.Suggestion; |
| import jdk.jshell.SnippetEvent; |
| import jdk.jshell.UnresolvedReferenceException; |
| import jdk.jshell.Snippet.SubKind; |
| import jdk.jshell.JShell.Subscription; |
| |
| 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.ResourceBundle; |
| import static java.util.stream.Collectors.toList; |
| |
| /** |
| * Command line REPL tool for Java using the JShell API. |
| * @author Robert Field |
| */ |
| public class JShellTool { |
| |
| private static final Pattern LINEBREAK = Pattern.compile("\\R"); |
| private static final Pattern HISTORY_ALL_FILENAME = Pattern.compile( |
| "((?<cmd>(all|history))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)"); |
| |
| final InputStream cmdin; |
| final PrintStream cmdout; |
| final PrintStream cmderr; |
| final PrintStream console; |
| final InputStream userin; |
| final PrintStream userout; |
| final PrintStream usererr; |
| |
| /** |
| * 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; |
| } |
| |
| 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 Feedback feedback = Feedback.Default; |
| private String cmdlineClasspath = null; |
| private String cmdlineStartup = null; |
| private String editor = null; |
| |
| static final Preferences PREFS = Preferences.userRoot().node("tool/REPL"); |
| |
| static final String STARTUP_KEY = "STARTUP"; |
| |
| 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 |
| NameSpace mainNamespace; |
| NameSpace startNamespace; |
| NameSpace errorNamespace; |
| NameSpace currentNameSpace; |
| Map<Snippet,SnippetInfo> mapSnippet; |
| |
| void debug(String format, Object... args) { |
| if (debug) { |
| cmderr.printf(format + "\n", args); |
| } |
| } |
| |
| /** |
| * For more verbose feedback modes |
| * @param format printf format |
| * @param args printf args |
| */ |
| void fluff(String format, Object... args) { |
| if (feedback() != Feedback.Off && feedback() != Feedback.Concise) { |
| hard(format, args); |
| } |
| } |
| |
| /** |
| * For concise feedback mode only |
| * @param format printf format |
| * @param args printf args |
| */ |
| void concise(String format, Object... args) { |
| if (feedback() == Feedback.Concise) { |
| hard(format, args); |
| } |
| } |
| |
| /** |
| * For all feedback modes -- must show |
| * @param format printf format |
| * @param args printf args |
| */ |
| void hard(String format, Object... args) { |
| cmdout.printf("| " + format + "\n", args); |
| } |
| |
| /** |
| * 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 |
| |
| for (String loadFile : loadList) { |
| cmdOpen(loadFile); |
| } |
| |
| if (regenerateOnDeath) { |
| fluff("Welcome to JShell -- Version %s", version()); |
| fluff("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() { |
| cmdout.printf("Usage: jshell <options> <load files>\n"); |
| cmdout.printf("where possible options include:\n"); |
| cmdout.printf(" -classpath <path> Specify where to find user class files\n"); |
| cmdout.printf(" -cp <path> Specify where to find user class files\n"); |
| cmdout.printf(" -startup <file> One run replacement for the start-up definitions\n"); |
| cmdout.printf(" -nostartup Do not run the start-up definitions\n"); |
| cmdout.printf(" -help Print a synopsis of standard options\n"); |
| cmdout.printf(" -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; |
| |
| 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. See /history"); |
| 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 (in.interactiveOutput() && displayPrompt) { |
| prompt = testPrompt |
| ? incomplete.isEmpty() |
| ? "\u0005" //ENQ |
| : "\u0006" //ACK |
| : incomplete.isEmpty() |
| ? feedback() == Feedback.Concise |
| ? "-> " |
| : "\n-> " |
| : ">> " |
| ; |
| } 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 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) { |
| 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 command = commands.get(cmd); |
| if (command == null || command.kind == CommandKind.HELP_ONLY) { |
| hard("No such command: %s", cmd); |
| fluff("Type /help for help."); |
| } else { |
| command.run.accept(arg); |
| } |
| } |
| |
| 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[] aliases; |
| public final String params; |
| public final String description; |
| public final Consumer<String> run; |
| public final CompletionProvider completions; |
| public final CommandKind kind; |
| |
| public Command(String command, String alias, String params, String description, Consumer<String> run, CompletionProvider completions) { |
| this(command, alias, params, description, run, completions, CommandKind.NORMAL); |
| } |
| |
| public Command(String command, String alias, String params, String description, Consumer<String> run, CompletionProvider completions, CommandKind kind) { |
| this.aliases = alias != null ? new String[] {command, alias} : new String[] {command}; |
| this.params = params; |
| this.description = description; |
| this.run = run; |
| this.completions = completions; |
| this.kind = kind; |
| } |
| |
| } |
| |
| interface CompletionProvider { |
| List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor); |
| } |
| |
| enum CommandKind { |
| NORMAL, |
| HIDDEN, |
| HELP_ONLY; |
| } |
| |
| 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 FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); |
| private final Map<String, Command> commands = new LinkedHashMap<>(); |
| private void registerCommand(Command cmd) { |
| for (String str : cmd.aliases) { |
| commands.put(str, 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 static CompletionProvider saveCompletion() { |
| CompletionProvider keyCompletion = new FixedCompletionProvider("all ", "history "); |
| return (code, cursor, anchor) -> { |
| List<Suggestion> result = new ArrayList<>(); |
| int space = code.indexOf(' '); |
| if (space == (-1)) { |
| result.addAll(keyCompletion.completionSuggestions(code, cursor, anchor)); |
| } |
| result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor)); |
| anchor[0] += space + 1; |
| return result; |
| }; |
| } |
| |
| // Table of commands -- with command forms, argument kinds, help message, implementation, ... |
| |
| { |
| registerCommand(new Command("/list", "/l", "[all]", "list the source you have typed", |
| arg -> cmdList(arg), |
| new FixedCompletionProvider("all"))); |
| registerCommand(new Command("/seteditor", null, "<executable>", "set the external editor command to use", |
| arg -> cmdSetEditor(arg), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/edit", "/e", "<name or id>", "edit a source entry referenced by name or id", |
| arg -> cmdEdit(arg), |
| editCompletion())); |
| registerCommand(new Command("/drop", "/d", "<name or id>", "delete a source entry referenced by name or id", |
| arg -> cmdDrop(arg), |
| editCompletion())); |
| registerCommand(new Command("/save", "/s", "[all|history] <file>", "save the source you have typed", |
| arg -> cmdSave(arg), |
| saveCompletion())); |
| registerCommand(new Command("/open", "/o", "<file>", "open a file as source input", |
| arg -> cmdOpen(arg), |
| FILE_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/vars", "/v", null, "list the declared variables and their values", |
| arg -> cmdVars(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/methods", "/m", null, "list the declared methods and their signatures", |
| arg -> cmdMethods(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/classes", "/c", null, "list the declared classes", |
| arg -> cmdClasses(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/exit", "/x", null, "exit the REPL", |
| arg -> cmdExit(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/reset", "/r", null, "reset everything in the REPL", |
| arg -> cmdReset(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/feedback", "/f", "<level>", "feedback information: off, concise, normal, verbose, default, or ?", |
| arg -> cmdFeedback(arg), |
| new FixedCompletionProvider("off", "concise", "normal", "verbose", "default", "?"))); |
| registerCommand(new Command("/prompt", "/p", null, "toggle display of a prompt", |
| arg -> cmdPrompt(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/classpath", "/cp", "<path>", "add a path to the classpath", |
| arg -> cmdClasspath(arg), |
| classPathCompletion())); |
| registerCommand(new Command("/history", "/h", null, "history of what you have typed", |
| arg -> cmdHistory(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/setstart", null, "<file>", "read file and set as the new start-up definitions", |
| arg -> cmdSetStart(arg), |
| FILE_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/savestart", null, "<file>", "save the default start-up definitions to the file", |
| arg -> cmdSaveStart(arg), |
| FILE_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/debug", "/db", "", "toggle debugging of the REPL", |
| arg -> cmdDebug(arg), |
| EMPTY_COMPLETION_PROVIDER, |
| CommandKind.HIDDEN)); |
| registerCommand(new Command("/help", "/?", "", "this help message", |
| arg -> cmdHelp(), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/!", null, "", "re-run last snippet", |
| arg -> cmdUseHistoryEntry(-1), |
| EMPTY_COMPLETION_PROVIDER)); |
| registerCommand(new Command("/<n>", null, "", "re-run n-th snippet", |
| arg -> { throw new IllegalStateException(); }, |
| EMPTY_COMPLETION_PROVIDER, |
| CommandKind.HELP_ONLY)); |
| registerCommand(new Command("/-<n>", null, "", "re-run n-th previous snippet", |
| arg -> { throw new IllegalStateException(); }, |
| EMPTY_COMPLETION_PROVIDER, |
| CommandKind.HELP_ONLY)); |
| } |
| |
| 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 != CommandKind.HIDDEN && cmd.kind != CommandKind.HELP_ONLY) |
| .map(cmd -> cmd.aliases[0]) |
| .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 command = commands.get(cmd); |
| if (command != null) { |
| result = command.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 --- |
| |
| void cmdSetEditor(String arg) { |
| if (arg.isEmpty()) { |
| hard("/seteditor requires a path argument"); |
| } else { |
| editor = arg; |
| fluff("Editor set to: %s", arg); |
| } |
| } |
| |
| void cmdClasspath(String arg) { |
| if (arg.isEmpty()) { |
| hard("/classpath requires a path argument"); |
| } else { |
| state.addToClasspath(toPathResolvingUserHome(arg).toString()); |
| fluff("Path %s added to classpath", arg); |
| } |
| } |
| |
| void 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"); |
| break; |
| } |
| } |
| InternalDebugControl.setDebugFlags(state, flags); |
| } |
| } |
| |
| private void cmdExit() { |
| regenerateOnDeath = false; |
| live = false; |
| fluff("Goodbye\n"); |
| } |
| |
| private void cmdFeedback(String arg) { |
| switch (arg) { |
| case "": |
| case "d": |
| case "default": |
| feedback = Feedback.Default; |
| break; |
| case "o": |
| case "off": |
| feedback = Feedback.Off; |
| break; |
| case "c": |
| case "concise": |
| feedback = Feedback.Concise; |
| break; |
| case "n": |
| case "normal": |
| feedback = Feedback.Normal; |
| break; |
| case "v": |
| case "verbose": |
| feedback = Feedback.Verbose; |
| break; |
| default: |
| hard("Follow /feedback with of the following:"); |
| hard(" off (errors and critical output only)"); |
| hard(" concise"); |
| hard(" normal"); |
| hard(" verbose"); |
| hard(" default"); |
| hard("You may also use just the first letter, for example: /f c"); |
| hard("In interactive mode 'default' is the same as 'normal', from a file it is the same as 'off'"); |
| return; |
| } |
| fluff("Feedback mode: %s", feedback.name().toLowerCase()); |
| } |
| |
| void cmdHelp() { |
| int synopsisLen = 0; |
| Map<String, String> synopsis2Description = new LinkedHashMap<>(); |
| for (Command cmd : new LinkedHashSet<>(commands.values())) { |
| if (cmd.kind == CommandKind.HIDDEN) |
| continue; |
| StringBuilder synopsis = new StringBuilder(); |
| if (cmd.aliases.length > 1) { |
| synopsis.append(String.format("%-3s or ", cmd.aliases[1])); |
| } else { |
| synopsis.append(" "); |
| } |
| synopsis.append(cmd.aliases[0]); |
| if (cmd.params != null) |
| synopsis.append(" ").append(cmd.params); |
| synopsis2Description.put(synopsis.toString(), cmd.description); |
| synopsisLen = Math.max(synopsisLen, synopsis.length()); |
| } |
| cmdout.println("Type a Java language expression, statement, or declaration."); |
| cmdout.println("Or type one of the following commands:\n"); |
| for (Entry<String, String> e : synopsis2Description.entrySet()) { |
| cmdout.print(String.format("%-" + synopsisLen + "s", e.getKey())); |
| cmdout.print(" -- "); |
| cmdout.println(e.getValue()); |
| } |
| cmdout.println(); |
| cmdout.println("Supported shortcuts include:"); |
| cmdout.println("<tab> -- show possible completions for the current text"); |
| cmdout.println("Shift-<tab> -- for current method or constructor invocation, show a synopsis of the method/constructor"); |
| } |
| |
| private void cmdHistory() { |
| cmdout.println(); |
| for (String s : input.currentSessionHistory()) { |
| // No number prefix, confusing with snippet ids |
| cmdout.printf("%s\n", s); |
| } |
| } |
| |
| /** |
| * Convert a user argument to a list of snippets referenced by that |
| * argument (or lack of argument). |
| * @param arg The user's argument to the command |
| * @return a list of referenced snippets |
| */ |
| private List<Snippet> argToSnippets(String arg) { |
| List<Snippet> snippets = new ArrayList<>(); |
| if (arg.isEmpty()) { |
| // Default is all user snippets |
| for (Snippet sn : state.snippets()) { |
| if (notInStartUp(sn)) { |
| snippets.add(sn); |
| } |
| } |
| } else { |
| // Look for all declarations with matching names |
| for (Snippet key : state.snippets()) { |
| switch (key.kind()) { |
| case METHOD: |
| case VAR: |
| case TYPE_DECL: |
| if (((DeclarationSnippet) key).name().equals(arg)) { |
| snippets.add(key); |
| } |
| break; |
| } |
| } |
| // If no declarations found, look for an id of this name |
| if (snippets.isEmpty()) { |
| for (Snippet sn : state.snippets()) { |
| if (sn.id().equals(arg)) { |
| snippets.add(sn); |
| break; |
| } |
| } |
| } |
| // If still no matches found, give an error |
| if (snippets.isEmpty()) { |
| hard("No definition or id named %s found. See /classes /methods /vars or /list", arg); |
| return null; |
| } |
| } |
| return snippets; |
| } |
| |
| private void 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; |
| } |
| List<Snippet> snippetSet = argToSnippets(arg); |
| if (snippetSet == null) { |
| return; |
| } |
| snippetSet = snippetSet.stream() |
| .filter(sn -> state.status(sn).isActive) |
| .collect(toList()); |
| snippetSet.removeIf(sn -> !(sn instanceof PersistentSnippet)); |
| if (snippetSet.isEmpty()) { |
| hard("The argument did not specify an import, variable, method, or class to drop."); |
| return; |
| } |
| if (snippetSet.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 : snippetSet) { |
| cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
| } |
| return; |
| } |
| PersistentSnippet psn = (PersistentSnippet) snippetSet.iterator().next(); |
| state.drop(psn).forEach(this::handleEvent); |
| } |
| |
| private void cmdEdit(String arg) { |
| List<Snippet> snippetSet = argToSnippets(arg); |
| if (snippetSet == null) { |
| return; |
| } |
| Set<String> srcSet = new LinkedHashSet<>(); |
| for (Snippet key : snippetSet) { |
| String src = key.source(); |
| switch (key.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); |
| } |
| } |
| //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 void cmdList(String arg) { |
| boolean all = false; |
| switch (arg) { |
| case "all": |
| all = true; |
| break; |
| case "history": |
| cmdHistory(); |
| return; |
| case "": |
| break; |
| default: |
| hard("Invalid /list argument: %s", arg); |
| return; |
| } |
| boolean hasOutput = false; |
| for (Snippet sn : state.snippets()) { |
| if (all || (notInStartUp(sn) && state.status(sn).isActive)) { |
| if (!hasOutput) { |
| cmdout.println(); |
| hasOutput = true; |
| } |
| cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
| |
| } |
| } |
| } |
| |
| private void cmdOpen(String filename) { |
| if (filename.isEmpty()) { |
| hard("The /open command requires a filename argument."); |
| } else { |
| try { |
| run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); |
| } catch (FileNotFoundException e) { |
| hard("File '%s' is not found: %s", filename, e.getMessage()); |
| } catch (Exception e) { |
| hard("Exception while reading file: %s", e); |
| } |
| } |
| } |
| |
| private void cmdPrompt() { |
| displayPrompt = !displayPrompt; |
| fluff("Prompt will %sdisplay. Use /prompt to toggle.", displayPrompt ? "" : "NOT "); |
| concise("Prompt: %s", displayPrompt ? "on" : "off"); |
| } |
| |
| private void cmdReset() { |
| live = false; |
| fluff("Resetting state."); |
| } |
| |
| private void cmdSave(String arg_filename) { |
| Matcher mat = HISTORY_ALL_FILENAME.matcher(arg_filename); |
| if (!mat.find()) { |
| hard("Malformed argument to the /save command: %s", arg_filename); |
| return; |
| } |
| boolean useHistory = false; |
| boolean saveAll = false; |
| String cmd = mat.group("cmd"); |
| if (cmd != null) switch (cmd) { |
| case "all": |
| saveAll = true; |
| break; |
| case "history": |
| useHistory = true; |
| break; |
| } |
| String filename = mat.group("filename"); |
| if (filename == null ||filename.isEmpty()) { |
| hard("The /save command requires a filename argument."); |
| return; |
| } |
| 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 { |
| for (Snippet sn : state.snippets()) { |
| if (saveAll || notInStartUp(sn)) { |
| writer.write(sn.source()); |
| writer.write("\n"); |
| } |
| } |
| } |
| } catch (FileNotFoundException e) { |
| hard("File '%s' for save is not accessible: %s", filename, e.getMessage()); |
| } catch (Exception e) { |
| hard("Exception while saving: %s", e); |
| } |
| } |
| |
| private void cmdSetStart(String filename) { |
| if (filename.isEmpty()) { |
| hard("The /setstart 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 /setstart is not accessible.", filename); |
| } catch (NoSuchFileException e) { |
| hard("File '%s' for /setstart is not found.", filename); |
| } catch (Exception e) { |
| hard("Exception while reading start set file: %s", e); |
| } |
| } |
| } |
| |
| private void cmdSaveStart(String filename) { |
| if (filename.isEmpty()) { |
| hard("The /savestart command requires a filename argument."); |
| } else { |
| try { |
| Files.write(toPathResolvingUserHome(filename), DEFAULT_STARTUP.getBytes()); |
| } catch (AccessDeniedException e) { |
| hard("File '%s' for /savestart is not accessible.", filename); |
| } catch (NoSuchFileException e) { |
| hard("File '%s' for /savestart cannot be located.", filename); |
| } catch (Exception e) { |
| hard("Exception while saving default startup file: %s", e); |
| } |
| } |
| } |
| |
| private void 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); |
| } |
| } |
| |
| private void cmdMethods() { |
| for (MethodSnippet mk : state.methods()) { |
| hard(" %s %s", mk.name(), mk.signature()); |
| } |
| } |
| |
| private void 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()); |
| } |
| } |
| |
| private void cmdUseHistoryEntry(int index) { |
| List<Snippet> keys = state.snippets(); |
| if (index < 0) |
| index += keys.size(); |
| else |
| index--; |
| if (index >= 0 && index < keys.size()) { |
| String source = keys.get(index).source(); |
| cmdout.printf("%s\n", source); |
| input.replaceLastHistoryEntry(source); |
| processSourceCatchingReset(source); |
| } else { |
| hard("Cannot find snippet %d", index + 1); |
| } |
| } |
| |
| /** |
| * 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")) { |
| 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; |
| List<SnippetEvent> events = state.eval(source); |
| for (SnippetEvent e : events) { |
| failed |= handleEvent(e); |
| } |
| 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 |
| SubKind subkind = sn.subKind(); |
| if (sn instanceof DeclarationSnippet |
| && (feedback() == Feedback.Verbose |
| || ste.status() == Status.OVERWRITTEN |
| || subkind == SubKind.VAR_DECLARATION_SUBKIND |
| || subkind == SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND)) { |
| // Under the conditions, display update information |
| displayDeclarationAndValue(ste, true, null); |
| 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(); |
| String declared; |
| Status status = ste.status(); |
| switch (status) { |
| case VALID: |
| case RECOVERABLE_DEFINED: |
| case RECOVERABLE_NOT_DEFINED: |
| if (ste.previousStatus().isActive) { |
| declared = ste.isSignatureChange() |
| ? "Replaced" |
| : "Modified"; |
| } else { |
| declared = "Added"; |
| } |
| break; |
| case OVERWRITTEN: |
| declared = "Overwrote"; |
| break; |
| case DROPPED: |
| declared = "Dropped"; |
| break; |
| case REJECTED: |
| declared = "Rejected"; |
| break; |
| case NONEXISTENT: |
| default: |
| // Should not occur |
| declared = ste.previousStatus().toString() + "=>" + status.toString(); |
| } |
| if (update) { |
| declared = " Update " + declared.toLowerCase(); |
| } |
| String however; |
| if (key instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { |
| String cannotUntil = (status == Status.RECOVERABLE_NOT_DEFINED) |
| ? " cannot be referenced until" |
| : " cannot be invoked until"; |
| however = (update? " which" : ", however, it") + cannotUntil + unresolved((DeclarationSnippet) key); |
| } else { |
| however = ""; |
| } |
| switch (key.subKind()) { |
| case CLASS_SUBKIND: |
| fluff("%s class %s%s", declared, ((TypeDeclSnippet) key).name(), however); |
| break; |
| case INTERFACE_SUBKIND: |
| fluff("%s interface %s%s", declared, ((TypeDeclSnippet) key).name(), however); |
| break; |
| case ENUM_SUBKIND: |
| fluff("%s enum %s%s", declared, ((TypeDeclSnippet) key).name(), however); |
| break; |
| case ANNOTATION_TYPE_SUBKIND: |
| fluff("%s annotation interface %s%s", declared, ((TypeDeclSnippet) key).name(), however); |
| break; |
| case METHOD_SUBKIND: |
| fluff("%s method %s(%s)%s", declared, ((MethodSnippet) key).name(), |
| ((MethodSnippet) key).parameterTypes(), however); |
| break; |
| case VAR_DECLARATION_SUBKIND: |
| if (!update) { |
| VarSnippet vk = (VarSnippet) key; |
| if (status == Status.RECOVERABLE_NOT_DEFINED) { |
| fluff("%s variable %s%s", declared, vk.name(), however); |
| } else { |
| fluff("%s variable %s of type %s%s", declared, vk.name(), vk.typeName(), however); |
| } |
| break; |
| } |
| // Fall through |
| case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { |
| VarSnippet vk = (VarSnippet) key; |
| if (status == Status.RECOVERABLE_NOT_DEFINED) { |
| if (!update) { |
| fluff("%s variable %s%s", declared, vk.name(), however); |
| break; |
| } |
| } else if (update) { |
| if (ste.isSignatureChange()) { |
| hard("%s variable %s, reset to null", declared, vk.name()); |
| } |
| } else { |
| fluff("%s variable %s of type %s with initial value %s", |
| declared, vk.name(), vk.typeName(), value); |
| concise("%s : %s", vk.name(), value); |
| } |
| break; |
| } |
| case TEMP_VAR_EXPRESSION_SUBKIND: { |
| VarSnippet vk = (VarSnippet) key; |
| if (update) { |
| hard("%s temporary variable %s, reset to null", declared, vk.name()); |
| } else { |
| fluff("Expression value is: %s", (value)); |
| fluff(" assigned to temporary variable %s of type %s", vk.name(), vk.typeName()); |
| concise("%s : %s", vk.name(), value); |
| } |
| break; |
| } |
| case OTHER_EXPRESSION_SUBKIND: |
| fluff("Expression value is: %s", (value)); |
| break; |
| case VAR_VALUE_SUBKIND: { |
| ExpressionSnippet ek = (ExpressionSnippet) key; |
| fluff("Variable %s of type %s has value %s", ek.name(), ek.typeName(), (value)); |
| concise("%s : %s", ek.name(), value); |
| break; |
| } |
| case ASSIGNMENT_SUBKIND: { |
| ExpressionSnippet ek = (ExpressionSnippet) key; |
| fluff("Variable %s has been assigned the value %s", ek.name(), (value)); |
| concise("%s : %s", ek.name(), value); |
| 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) { |
| MethodSnippet corralled = ex.getMethodSnippet(); |
| 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("."); |
| } |
| |
| hard("Attempted to call %s which cannot be invoked until%s", 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(", "); |
| } |
| } |
| switch (unr.size()) { |
| case 0: |
| break; |
| case 1: |
| sb.append(" is declared"); |
| break; |
| default: |
| sb.append(" are declared"); |
| break; |
| } |
| return sb.toString(); |
| } |
| |
| enum Feedback { |
| Default, |
| Off, |
| Concise, |
| Normal, |
| Verbose |
| } |
| |
| Feedback feedback() { |
| if (feedback == Feedback.Default) { |
| return input == null || input.interactiveOutput() ? Feedback.Normal : Feedback.Off; |
| } |
| return feedback; |
| } |
| |
| boolean notInStartUp(Snippet sn) { |
| return mapSnippet.get(sn).space != startNamespace; |
| } |
| |
| /** 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; |
| } |
| } |
| } |
| |
| class ScannerIOContext extends IOContext { |
| |
| private final Scanner scannerIn; |
| private final PrintStream pStream; |
| |
| public ScannerIOContext(Scanner scannerIn, PrintStream pStream) { |
| this.scannerIn = scannerIn; |
| this.pStream = pStream; |
| } |
| |
| @Override |
| public String readLine(String prompt, String prefix) { |
| if (pStream != null && prompt != null) { |
| pStream.print(prompt); |
| } |
| if (scannerIn.hasNextLine()) { |
| return scannerIn.nextLine(); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean interactiveOutput() { |
| return true; |
| } |
| |
| @Override |
| public Iterable<String> currentSessionHistory() { |
| return Collections.emptyList(); |
| } |
| |
| @Override |
| public void close() { |
| scannerIn.close(); |
| } |
| |
| @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 FileScannerIOContext extends ScannerIOContext { |
| |
| public FileScannerIOContext(String fn) throws FileNotFoundException { |
| this(new FileReader(fn)); |
| } |
| |
| public FileScannerIOContext(Reader rdr) throws FileNotFoundException { |
| super(new Scanner(rdr), null); |
| } |
| |
| @Override |
| public boolean interactiveOutput() { |
| return false; |
| } |
| } |
| |