1 /*
   2  * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.internal.jshell.tool;
  27 
  28 import java.io.BufferedWriter;
  29 import java.io.ByteArrayInputStream;
  30 import java.io.File;
  31 import java.io.FileNotFoundException;
  32 import java.io.FileReader;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.PrintStream;
  36 import java.io.Reader;
  37 import java.io.StringReader;
  38 import java.nio.charset.Charset;
  39 import java.nio.file.AccessDeniedException;
  40 import java.nio.file.FileSystems;
  41 import java.nio.file.Files;
  42 import java.nio.file.NoSuchFileException;
  43 import java.nio.file.Path;
  44 import java.nio.file.Paths;
  45 import java.text.MessageFormat;
  46 import java.util.ArrayList;
  47 import java.util.Arrays;
  48 import java.util.Collections;
  49 import java.util.Iterator;
  50 import java.util.LinkedHashMap;
  51 import java.util.LinkedHashSet;
  52 import java.util.List;
  53 import java.util.Locale;
  54 import java.util.Map;
  55 import java.util.Map.Entry;
  56 import java.util.Scanner;
  57 import java.util.Set;
  58 import java.util.function.Consumer;
  59 import java.util.function.Predicate;
  60 import java.util.prefs.Preferences;
  61 import java.util.regex.Matcher;
  62 import java.util.regex.Pattern;
  63 import java.util.stream.Collectors;
  64 import java.util.stream.Stream;
  65 import java.util.stream.StreamSupport;
  66 
  67 import jdk.internal.jshell.debug.InternalDebugControl;
  68 import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
  69 import jdk.jshell.DeclarationSnippet;
  70 import jdk.jshell.Diag;
  71 import jdk.jshell.EvalException;
  72 import jdk.jshell.ExpressionSnippet;
  73 import jdk.jshell.ImportSnippet;
  74 import jdk.jshell.JShell;
  75 import jdk.jshell.JShell.Subscription;
  76 import jdk.jshell.MethodSnippet;
  77 import jdk.jshell.PersistentSnippet;
  78 import jdk.jshell.Snippet;
  79 import jdk.jshell.Snippet.Status;
  80 import jdk.jshell.SnippetEvent;
  81 import jdk.jshell.SourceCodeAnalysis;
  82 import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
  83 import jdk.jshell.SourceCodeAnalysis.Suggestion;
  84 import jdk.jshell.TypeDeclSnippet;
  85 import jdk.jshell.UnresolvedReferenceException;
  86 import jdk.jshell.VarSnippet;
  87 
  88 import static java.nio.file.StandardOpenOption.CREATE;
  89 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
  90 import static java.nio.file.StandardOpenOption.WRITE;
  91 import java.util.MissingResourceException;
  92 import java.util.Optional;
  93 import java.util.ResourceBundle;
  94 import java.util.Spliterators;
  95 import java.util.function.Function;
  96 import java.util.function.Supplier;
  97 import jdk.internal.jshell.tool.Feedback.FormatAction;
  98 import jdk.internal.jshell.tool.Feedback.FormatCase;
  99 import jdk.internal.jshell.tool.Feedback.FormatErrors;
 100 import jdk.internal.jshell.tool.Feedback.FormatResolve;
 101 import jdk.internal.jshell.tool.Feedback.FormatUnresolved;
 102 import jdk.internal.jshell.tool.Feedback.FormatWhen;
 103 import static java.util.stream.Collectors.toList;
 104 import static java.util.stream.Collectors.toMap;
 105 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND;
 106 
 107 /**
 108  * Command line REPL tool for Java using the JShell API.
 109  * @author Robert Field
 110  */
 111 public class JShellTool {
 112 
 113     private static final String LINE_SEP = System.getProperty("line.separator");
 114     private static final Pattern LINEBREAK = Pattern.compile("\\R");
 115     private static final Pattern HISTORY_ALL_START_FILENAME = Pattern.compile(
 116             "((?<cmd>(all|history|start))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)");
 117     private static final String RECORD_SEPARATOR = "\u241E";
 118     private static final String RB_NAME_PREFIX  = "jdk.internal.jshell.tool.resources";
 119     private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version";
 120     private static final String L10N_RB_NAME    = RB_NAME_PREFIX + ".l10n";
 121 
 122     final InputStream cmdin;
 123     final PrintStream cmdout;
 124     final PrintStream cmderr;
 125     final PrintStream console;
 126     final InputStream userin;
 127     final PrintStream userout;
 128     final PrintStream usererr;
 129     final Preferences prefs;
 130     final Locale locale;
 131 
 132     final Feedback feedback = new Feedback();
 133 
 134     /**
 135      * The constructor for the tool (used by tool launch via main and by test
 136      * harnesses to capture ins and outs.
 137      * @param cmdin command line input -- snippets and commands
 138      * @param cmdout command line output, feedback including errors
 139      * @param cmderr start-up errors and debugging info
 140      * @param console console control interaction
 141      * @param userin code execution input (not yet functional)
 142      * @param userout code execution output  -- System.out.printf("hi")
 143      * @param usererr code execution error stream  -- System.err.printf("Oops")
 144      * @param prefs preferences to use
 145      * @param locale locale to use
 146      */
 147     public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
 148             PrintStream console,
 149             InputStream userin, PrintStream userout, PrintStream usererr,
 150             Preferences prefs, Locale locale) {
 151         this.cmdin = cmdin;
 152         this.cmdout = cmdout;
 153         this.cmderr = cmderr;
 154         this.console = console;
 155         this.userin = userin;
 156         this.userout = userout;
 157         this.usererr = usererr;
 158         this.prefs = prefs;
 159         this.locale = locale;
 160     }
 161 
 162     private ResourceBundle versionRB = null;
 163     private ResourceBundle outputRB  = null;
 164 
 165     private IOContext input = null;
 166     private boolean regenerateOnDeath = true;
 167     private boolean live = false;
 168     private boolean feedbackInitialized = false;
 169 
 170     SourceCodeAnalysis analysis;
 171     JShell state = null;
 172     Subscription shutdownSubscription = null;
 173 
 174     private boolean debug = false;
 175     public boolean testPrompt = false;
 176     private String cmdlineClasspath = null;
 177     private String cmdlineStartup = null;
 178     private String[] editor = null;
 179 
 180     // Commands and snippets which should be replayed
 181     private List<String> replayableHistory;
 182     private List<String> replayableHistoryPrevious;
 183 
 184     static final String STARTUP_KEY = "STARTUP";
 185     static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE";
 186 
 187     static final String DEFAULT_STARTUP =
 188             "\n" +
 189             "import java.util.*;\n" +
 190             "import java.io.*;\n" +
 191             "import java.math.*;\n" +
 192             "import java.net.*;\n" +
 193             "import java.util.concurrent.*;\n" +
 194             "import java.util.prefs.*;\n" +
 195             "import java.util.regex.*;\n" +
 196             "void printf(String format, Object... args) { System.out.printf(format, args); }\n";
 197 
 198     // Tool id (tid) mapping: the three name spaces
 199     NameSpace mainNamespace;
 200     NameSpace startNamespace;
 201     NameSpace errorNamespace;
 202 
 203     // Tool id (tid) mapping: the current name spaces
 204     NameSpace currentNameSpace;
 205 
 206     Map<Snippet,SnippetInfo> mapSnippet;
 207 
 208     /**
 209      * Is the input/output currently interactive
 210      *
 211      * @return true if console
 212      */
 213     boolean interactive() {
 214         return input != null && input.interactiveOutput();
 215     }
 216 
 217     void debug(String format, Object... args) {
 218         if (debug) {
 219             cmderr.printf(format + "\n", args);
 220         }
 221     }
 222 
 223     /**
 224      * Base output for command output -- no pre- or post-fix
 225      *
 226      * @param printf format
 227      * @param printf args
 228      */
 229     void rawout(String format, Object... args) {
 230         cmdout.printf(format, args);
 231     }
 232 
 233     /**
 234      * Must show command output
 235      *
 236      * @param format printf format
 237      * @param args printf args
 238      */
 239     void hard(String format, Object... args) {
 240         rawout(feedback.getPre() + format + feedback.getPost(), args);
 241     }
 242 
 243     /**
 244      * Error command output
 245      *
 246      * @param format printf format
 247      * @param args printf args
 248      */
 249     void error(String format, Object... args) {
 250         rawout(feedback.getErrorPre() + format + feedback.getErrorPost(), args);
 251     }
 252 
 253     /**
 254      * Optional output
 255      *
 256      * @param format printf format
 257      * @param args printf args
 258      */
 259     void fluff(String format, Object... args) {
 260         if (feedback.shouldDisplayCommandFluff() && interactive()) {
 261             hard(format, args);
 262         }
 263     }
 264 
 265     /**
 266      * Optional output -- with embedded per- and post-fix
 267      *
 268      * @param format printf format
 269      * @param args printf args
 270      */
 271     void fluffRaw(String format, Object... args) {
 272         if (feedback.shouldDisplayCommandFluff() && interactive()) {
 273             rawout(format, args);
 274         }
 275     }
 276 
 277     /**
 278      * Print using resource bundle look-up and adding prefix and postfix
 279      *
 280      * @param key the resource key
 281      */
 282     String getResourceString(String key) {
 283         if (outputRB == null) {
 284             try {
 285                 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale);
 286             } catch (MissingResourceException mre) {
 287                 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale);
 288                 return "";
 289             }
 290         }
 291         String s;
 292         try {
 293             s = outputRB.getString(key);
 294         } catch (MissingResourceException mre) {
 295             error("Missing resource: %s in %s", key, L10N_RB_NAME);
 296             return "";
 297         }
 298         return s;
 299     }
 300 
 301     /**
 302      * Add prefixing to embedded newlines in a string, leading with the normal
 303      * prefix
 304      *
 305      * @param s the string to prefix
 306      */
 307     String prefix(String s) {
 308         return prefix(s, feedback.getPre());
 309     }
 310 
 311     /**
 312      * Add prefixing to embedded newlines in a string
 313      *
 314      * @param s the string to prefix
 315      * @param leading the string to prepend
 316      */
 317     String prefix(String s, String leading) {
 318         if (s == null || s.isEmpty()) {
 319             return "";
 320         }
 321         return leading
 322                 + s.substring(0, s.length() - 1).replaceAll("\\R", System.getProperty("line.separator") + feedback.getPre())
 323                 + s.substring(s.length() - 1, s.length());
 324     }
 325 
 326     /**
 327      * Print using resource bundle look-up and adding prefix and postfix
 328      *
 329      * @param key the resource key
 330      */
 331     void hardrb(String key) {
 332         String s = prefix(getResourceString(key));
 333         cmdout.println(s);
 334     }
 335 
 336     /**
 337      * Format using resource bundle look-up using MessageFormat
 338      *
 339      * @param key the resource key
 340      * @param args
 341      */
 342     String messageFormat(String key, Object... args) {
 343         String rs = getResourceString(key);
 344         return MessageFormat.format(rs, args);
 345     }
 346 
 347     /**
 348      * Print using resource bundle look-up, MessageFormat, and add prefix and
 349      * postfix
 350      *
 351      * @param key the resource key
 352      * @param args
 353      */
 354     void hardmsg(String key, Object... args) {
 355         cmdout.println(prefix(messageFormat(key, args)));
 356     }
 357 
 358     /**
 359      * Print error using resource bundle look-up, MessageFormat, and add prefix
 360      * and postfix
 361      *
 362      * @param key the resource key
 363      * @param args
 364      */
 365     void errormsg(String key, Object... args) {
 366         cmdout.println(prefix(messageFormat(key, args), feedback.getErrorPre()));
 367     }
 368 
 369     /**
 370      * Print command-line error using resource bundle look-up, MessageFormat
 371      *
 372      * @param key the resource key
 373      * @param args
 374      */
 375     void startmsg(String key, Object... args) {
 376         cmderr.println(prefix(messageFormat(key, args), ""));
 377     }
 378 
 379     /**
 380      * Print (fluff) using resource bundle look-up, MessageFormat, and add
 381      * prefix and postfix
 382      *
 383      * @param key the resource key
 384      * @param args
 385      */
 386     void fluffmsg(String key, Object... args) {
 387         if (feedback.shouldDisplayCommandFluff() && interactive()) {
 388             hardmsg(key, args);
 389         }
 390     }
 391 
 392     <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
 393         Map<String, String> a2b = stream.collect(toMap(a, b,
 394                 (m1, m2) -> m1,
 395                 () -> new LinkedHashMap<>()));
 396         int aLen = 0;
 397         for (String av : a2b.keySet()) {
 398             aLen = Math.max(aLen, av.length());
 399         }
 400         String format = "   %-" + aLen + "s -- %s";
 401         String indentedNewLine = LINE_SEP + feedback.getPre()
 402                 + String.format("   %-" + (aLen + 4) + "s", "");
 403         for (Entry<String, String> e : a2b.entrySet()) {
 404             hard(format, e.getKey(), e.getValue().replaceAll("\n", indentedNewLine));
 405         }
 406     }
 407 
 408     /**
 409      * Trim whitespace off end of string
 410      *
 411      * @param s
 412      * @return
 413      */
 414     static String trimEnd(String s) {
 415         int last = s.length() - 1;
 416         int i = last;
 417         while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
 418             --i;
 419         }
 420         if (i != last) {
 421             return s.substring(0, i + 1);
 422         } else {
 423             return s;
 424         }
 425     }
 426 
 427     /**
 428      * Normal start entry point
 429      * @param args
 430      * @throws Exception
 431      */
 432     public static void main(String[] args) throws Exception {
 433         new JShellTool(System.in, System.out, System.err, System.out,
 434                  new ByteArrayInputStream(new byte[0]), System.out, System.err,
 435                  Preferences.userRoot().node("tool/JShell"),
 436                  Locale.getDefault())
 437                 .start(args);
 438     }
 439 
 440     public void start(String[] args) throws Exception {
 441         List<String> loadList = processCommandArgs(args);
 442         if (loadList == null) {
 443             // Abort
 444             return;
 445         }
 446         try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
 447             start(in, loadList);
 448         }
 449     }
 450 
 451     private void start(IOContext in, List<String> loadList) {
 452         resetState(); // Initialize
 453 
 454         // Read replay history from last jshell session into previous history
 455         String prevReplay = prefs.get(REPLAY_RESTORE_KEY, null);
 456         if (prevReplay != null) {
 457             replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR));
 458         }
 459 
 460         for (String loadFile : loadList) {
 461             runFile(loadFile, "jshell");
 462         }
 463 
 464         if (regenerateOnDeath) {
 465             hardmsg("jshell.msg.welcome", version());
 466         }
 467 
 468         try {
 469             while (regenerateOnDeath) {
 470                 if (!live) {
 471                     resetState();
 472                 }
 473                 run(in);
 474             }
 475         } finally {
 476             closeState();
 477         }
 478     }
 479 
 480     /**
 481      * Process the command line arguments.
 482      * Set options.
 483      * @param args the command line arguments
 484      * @return the list of files to be loaded
 485      */
 486     private List<String> processCommandArgs(String[] args) {
 487         List<String> loadList = new ArrayList<>();
 488         Iterator<String> ai = Arrays.asList(args).iterator();
 489         while (ai.hasNext()) {
 490             String arg = ai.next();
 491             if (arg.startsWith("-")) {
 492                 switch (arg) {
 493                     case "-classpath":
 494                     case "-cp":
 495                         if (cmdlineClasspath != null) {
 496                             startmsg("jshell.err.opt.classpath.conflict");
 497                             return null;
 498                         }
 499                         if (ai.hasNext()) {
 500                             cmdlineClasspath = ai.next();
 501                         } else {
 502                             startmsg("jshell.err.opt.classpath.arg");
 503                             return null;
 504                         }
 505                         break;
 506                     case "-help":
 507                         printUsage();
 508                         return null;
 509                     case "-version":
 510                         cmdout.printf("jshell %s\n", version());
 511                         return null;
 512                     case "-fullversion":
 513                         cmdout.printf("jshell %s\n", fullVersion());
 514                         return null;
 515                     case "-startup":
 516                         if (cmdlineStartup != null) {
 517                             startmsg("jshell.err.opt.startup.conflict");
 518                             return null;
 519                         }
 520                         cmdlineStartup = readFile(ai.hasNext()? ai.next() : null, "'-startup'");
 521                         if (cmdlineStartup == null) {
 522                             return null;
 523                         }
 524                         break;
 525                     case "-nostartup":
 526                         if (cmdlineStartup != null && !cmdlineStartup.isEmpty()) {
 527                             startmsg("jshell.err.opt.startup.conflict");
 528                             return null;
 529                         }
 530                         cmdlineStartup = "";
 531                         break;
 532                     default:
 533                         startmsg("jshell.err.opt.unknown", arg);
 534                         printUsage();
 535                         return null;
 536                 }
 537             } else {
 538                 loadList.add(arg);
 539             }
 540         }
 541         return loadList;
 542     }
 543 
 544     private void printUsage() {
 545         cmdout.print(getResourceString("help.usage"));
 546     }
 547 
 548     private void resetState() {
 549         closeState();
 550 
 551         // Initialize tool id mapping
 552         mainNamespace = new NameSpace("main", "");
 553         startNamespace = new NameSpace("start", "s");
 554         errorNamespace = new NameSpace("error", "e");
 555         mapSnippet = new LinkedHashMap<>();
 556         currentNameSpace = startNamespace;
 557 
 558         // Reset the replayable history, saving the old for restore
 559         replayableHistoryPrevious = replayableHistory;
 560         replayableHistory = new ArrayList<>();
 561 
 562         state = JShell.builder()
 563                 .in(userin)
 564                 .out(userout)
 565                 .err(usererr)
 566                 .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext())
 567                 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive)
 568                         ? currentNameSpace.tid(sn)
 569                         : errorNamespace.tid(sn))
 570                 .build();
 571         shutdownSubscription = state.onShutdown((JShell deadState) -> {
 572             if (deadState == state) {
 573                 hardmsg("jshell.msg.terminated");
 574                 live = false;
 575             }
 576         });
 577         analysis = state.sourceCodeAnalysis();
 578         live = true;
 579         if (!feedbackInitialized) {
 580             startUpRun(getResourceString("startup.feedback"));
 581             feedbackInitialized = true;
 582         }
 583 
 584         if (cmdlineClasspath != null) {
 585             state.addToClasspath(cmdlineClasspath);
 586         }
 587 
 588         String start;
 589         if (cmdlineStartup == null) {
 590             start = prefs.get(STARTUP_KEY, "<nada>");
 591             if (start.equals("<nada>")) {
 592                 start = DEFAULT_STARTUP;
 593                 prefs.put(STARTUP_KEY, DEFAULT_STARTUP);
 594             }
 595         } else {
 596             start = cmdlineStartup;
 597         }
 598         startUpRun(start);
 599         currentNameSpace = mainNamespace;
 600     }
 601     //where
 602     private void startUpRun(String start) {
 603         try (IOContext suin = new FileScannerIOContext(new StringReader(start))) {
 604             run(suin);
 605         } catch (Exception ex) {
 606             hardmsg("jshell.err.startup.unexpected.exception", ex);
 607         }
 608     }
 609 
 610     private void closeState() {
 611         live = false;
 612         JShell oldState = state;
 613         if (oldState != null) {
 614             oldState.unsubscribe(shutdownSubscription); // No notification
 615             oldState.close();
 616         }
 617     }
 618 
 619     /**
 620      * Main loop
 621      * @param in the line input/editing context
 622      */
 623     private void run(IOContext in) {
 624         IOContext oldInput = input;
 625         input = in;
 626         try {
 627             String incomplete = "";
 628             while (live) {
 629                 String prompt;
 630                 if (currentNameSpace == mainNamespace) {
 631                     prompt = testPrompt
 632                                     ? incomplete.isEmpty()
 633                                             ? "\u0005" //ENQ
 634                                             : "\u0006" //ACK
 635                                     : incomplete.isEmpty()
 636                                             ? feedback.getPrompt(currentNameSpace.tidNext())
 637                                             : feedback.getContinuationPrompt(currentNameSpace.tidNext())
 638                     ;
 639                 } else {
 640                     prompt = "";
 641                 }
 642                 String raw;
 643                 try {
 644                     raw = in.readLine(prompt, incomplete);
 645                 } catch (InputInterruptedException ex) {
 646                     //input interrupted - clearing current state
 647                     incomplete = "";
 648                     continue;
 649                 }
 650                 if (raw == null) {
 651                     //EOF
 652                     if (in.interactiveOutput()) {
 653                         // End after user ctrl-D
 654                         regenerateOnDeath = false;
 655                     }
 656                     break;
 657                 }
 658                 String trimmed = trimEnd(raw);
 659                 if (!trimmed.isEmpty()) {
 660                     String line = incomplete + trimmed;
 661 
 662                     // No commands in the middle of unprocessed source
 663                     if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) {
 664                         processCommand(line.trim());
 665                     } else {
 666                         incomplete = processSourceCatchingReset(line);
 667                     }
 668                 }
 669             }
 670         } catch (IOException ex) {
 671             errormsg("jshell.err.unexpected.exception", ex);
 672         } finally {
 673             input = oldInput;
 674         }
 675     }
 676 
 677     private void addToReplayHistory(String s) {
 678         if (currentNameSpace == mainNamespace) {
 679             replayableHistory.add(s);
 680         }
 681     }
 682 
 683     private String processSourceCatchingReset(String src) {
 684         try {
 685             input.beforeUserCode();
 686             return processSource(src);
 687         } catch (IllegalStateException ex) {
 688             hard("Resetting...");
 689             live = false; // Make double sure
 690             return "";
 691         } finally {
 692             input.afterUserCode();
 693         }
 694     }
 695 
 696     private void processCommand(String cmd) {
 697         if (cmd.startsWith("/-")) {
 698             try {
 699                 //handle "/-[number]"
 700                 cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1)));
 701                 return ;
 702             } catch (NumberFormatException ex) {
 703                 //ignore
 704             }
 705         }
 706         String arg = "";
 707         int idx = cmd.indexOf(' ');
 708         if (idx > 0) {
 709             arg = cmd.substring(idx + 1).trim();
 710             cmd = cmd.substring(0, idx);
 711         }
 712         Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand);
 713         switch (candidates.length) {
 714             case 0:
 715                 if (!rerunHistoryEntryById(cmd.substring(1))) {
 716                     errormsg("jshell.err.no.such.command.or.snippet.id", cmd);
 717                     fluffmsg("jshell.msg.help.for.help");
 718                 }   break;
 719             case 1:
 720                 Command command = candidates[0];
 721                 // If comand was successful and is of a replayable kind, add it the replayable history
 722                 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) {
 723                     addToReplayHistory((command.command + " " + arg).trim());
 724                 }   break;
 725             default:
 726                 errormsg("jshell.err.command.ambiguous", cmd,
 727                         Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
 728                 fluffmsg("jshell.msg.help.for.help");
 729                 break;
 730         }
 731     }
 732 
 733     private Command[] findCommand(String cmd, Predicate<Command> filter) {
 734         Command exact = commands.get(cmd);
 735         if (exact != null)
 736             return new Command[] {exact};
 737 
 738         return commands.values()
 739                        .stream()
 740                        .filter(filter)
 741                        .filter(command -> command.command.startsWith(cmd))
 742                        .toArray(size -> new Command[size]);
 743     }
 744 
 745     private static Path toPathResolvingUserHome(String pathString) {
 746         if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
 747             return Paths.get(System.getProperty("user.home"), pathString.substring(2));
 748         else
 749             return Paths.get(pathString);
 750     }
 751 
 752     static final class Command {
 753         public final String command;
 754         public final String helpKey;
 755         public final Function<String,Boolean> run;
 756         public final CompletionProvider completions;
 757         public final CommandKind kind;
 758 
 759         // NORMAL Commands
 760         public Command(String command, Function<String,Boolean> run, CompletionProvider completions) {
 761             this(command, run, completions, CommandKind.NORMAL);
 762         }
 763 
 764         // Special kinds of Commands
 765         public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
 766             this(command, "help." + command.substring(1),
 767                     run, completions, kind);
 768         }
 769 
 770         // Documentation pseudo-commands
 771         public Command(String command, String helpKey, CommandKind kind) {
 772             this(command, helpKey,
 773                     arg -> { throw new IllegalStateException(); },
 774                     EMPTY_COMPLETION_PROVIDER,
 775                     kind);
 776         }
 777 
 778         public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
 779             this.command = command;
 780             this.helpKey = helpKey;
 781             this.run = run;
 782             this.completions = completions;
 783             this.kind = kind;
 784         }
 785 
 786     }
 787 
 788     interface CompletionProvider {
 789         List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
 790     }
 791 
 792     enum CommandKind {
 793         NORMAL(true, true, true),
 794         REPLAY(true, true, true),
 795         HIDDEN(true, false, false),
 796         HELP_ONLY(false, true, false),
 797         HELP_SUBJECT(false, false, false);
 798 
 799         final boolean isRealCommand;
 800         final boolean showInHelp;
 801         final boolean shouldSuggestCompletions;
 802         private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) {
 803             this.isRealCommand = isRealCommand;
 804             this.showInHelp = showInHelp;
 805             this.shouldSuggestCompletions = shouldSuggestCompletions;
 806         }
 807     }
 808 
 809     static final class FixedCompletionProvider implements CompletionProvider {
 810 
 811         private final String[] alternatives;
 812 
 813         public FixedCompletionProvider(String... alternatives) {
 814             this.alternatives = alternatives;
 815         }
 816 
 817         @Override
 818         public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
 819             List<Suggestion> result = new ArrayList<>();
 820 
 821             for (String alternative : alternatives) {
 822                 if (alternative.startsWith(input)) {
 823                     result.add(new Suggestion(alternative, false));
 824                 }
 825             }
 826 
 827             anchor[0] = 0;
 828 
 829             return result;
 830         }
 831 
 832     }
 833 
 834     private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
 835     private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("all ", "start ", "history ");
 836     private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("restore", "quiet");
 837     private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
 838     private final Map<String, Command> commands = new LinkedHashMap<>();
 839     private void registerCommand(Command cmd) {
 840         commands.put(cmd.command, cmd);
 841     }
 842     private static CompletionProvider fileCompletions(Predicate<Path> accept) {
 843         return (code, cursor, anchor) -> {
 844             int lastSlash = code.lastIndexOf('/');
 845             String path = code.substring(0, lastSlash + 1);
 846             String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
 847             Path current = toPathResolvingUserHome(path);
 848             List<Suggestion> result = new ArrayList<>();
 849             try (Stream<Path> dir = Files.list(current)) {
 850                 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
 851                    .map(f -> new Suggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""), false))
 852                    .forEach(result::add);
 853             } catch (IOException ex) {
 854                 //ignore...
 855             }
 856             if (path.isEmpty()) {
 857                 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
 858                              .filter(root -> accept.test(root) && root.toString().startsWith(prefix))
 859                              .map(root -> new Suggestion(root.toString(), false))
 860                              .forEach(result::add);
 861             }
 862             anchor[0] = path.length();
 863             return result;
 864         };
 865     }
 866 
 867     private static CompletionProvider classPathCompletion() {
 868         return fileCompletions(p -> Files.isDirectory(p) ||
 869                                     p.getFileName().toString().endsWith(".zip") ||
 870                                     p.getFileName().toString().endsWith(".jar"));
 871     }
 872 
 873     private CompletionProvider editCompletion() {
 874         return (prefix, cursor, anchor) -> {
 875             anchor[0] = 0;
 876             return state.snippets()
 877                         .stream()
 878                         .flatMap(k -> (k instanceof DeclarationSnippet)
 879                                 ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name())
 880                                 : Stream.of(String.valueOf(k.id())))
 881                         .filter(k -> k.startsWith(prefix))
 882                         .map(k -> new Suggestion(k, false))
 883                         .collect(Collectors.toList());
 884         };
 885     }
 886 
 887     private CompletionProvider editKeywordCompletion() {
 888         return (code, cursor, anchor) -> {
 889             List<Suggestion> result = new ArrayList<>();
 890             result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
 891             result.addAll(editCompletion().completionSuggestions(code, cursor, anchor));
 892             return result;
 893         };
 894     }
 895 
 896     private static CompletionProvider saveCompletion() {
 897         return (code, cursor, anchor) -> {
 898             List<Suggestion> result = new ArrayList<>();
 899             int space = code.indexOf(' ');
 900             if (space == (-1)) {
 901                 result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
 902             }
 903             result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
 904             anchor[0] += space + 1;
 905             return result;
 906         };
 907     }
 908 
 909     private static CompletionProvider reloadCompletion() {
 910         return (code, cursor, anchor) -> {
 911             List<Suggestion> result = new ArrayList<>();
 912             int pastSpace = code.indexOf(' ') + 1; // zero if no space
 913             result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor));
 914             anchor[0] += pastSpace;
 915             return result;
 916         };
 917     }
 918 
 919     // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ...
 920 
 921     {
 922         registerCommand(new Command("/list",
 923                 arg -> cmdList(arg),
 924                 editKeywordCompletion()));
 925         registerCommand(new Command("/edit",
 926                 arg -> cmdEdit(arg),
 927                 editCompletion()));
 928         registerCommand(new Command("/drop",
 929                 arg -> cmdDrop(arg),
 930                 editCompletion(),
 931                 CommandKind.REPLAY));
 932         registerCommand(new Command("/save",
 933                 arg -> cmdSave(arg),
 934                 saveCompletion()));
 935         registerCommand(new Command("/open",
 936                 arg -> cmdOpen(arg),
 937                 FILE_COMPLETION_PROVIDER));
 938         registerCommand(new Command("/vars",
 939                 arg -> cmdVars(),
 940                 EMPTY_COMPLETION_PROVIDER));
 941         registerCommand(new Command("/methods",
 942                 arg -> cmdMethods(),
 943                 EMPTY_COMPLETION_PROVIDER));
 944         registerCommand(new Command("/classes",
 945                 arg -> cmdClasses(),
 946                 EMPTY_COMPLETION_PROVIDER));
 947         registerCommand(new Command("/imports",
 948                 arg -> cmdImports(),
 949                 EMPTY_COMPLETION_PROVIDER));
 950         registerCommand(new Command("/exit",
 951                 arg -> cmdExit(),
 952                 EMPTY_COMPLETION_PROVIDER));
 953         registerCommand(new Command("/reset",
 954                 arg -> cmdReset(),
 955                 EMPTY_COMPLETION_PROVIDER));
 956         registerCommand(new Command("/reload",
 957                 arg -> cmdReload(arg),
 958                 reloadCompletion()));
 959         registerCommand(new Command("/classpath",
 960                 arg -> cmdClasspath(arg),
 961                 classPathCompletion(),
 962                 CommandKind.REPLAY));
 963         registerCommand(new Command("/history",
 964                 arg -> cmdHistory(),
 965                 EMPTY_COMPLETION_PROVIDER));
 966         registerCommand(new Command("/debug",
 967                 arg -> cmdDebug(arg),
 968                 EMPTY_COMPLETION_PROVIDER,
 969                 CommandKind.HIDDEN));
 970         registerCommand(new Command("/help",
 971                 arg -> cmdHelp(arg),
 972                 EMPTY_COMPLETION_PROVIDER));
 973         registerCommand(new Command("/set",
 974                 arg -> cmdSet(arg),
 975                 new FixedCompletionProvider("format", "feedback", "prompt", "newmode", "start", "editor")));
 976         registerCommand(new Command("/?",
 977                 "help.quest",
 978                 arg -> cmdHelp(arg),
 979                 EMPTY_COMPLETION_PROVIDER,
 980                 CommandKind.NORMAL));
 981         registerCommand(new Command("/!",
 982                 "help.bang",
 983                 arg -> cmdUseHistoryEntry(-1),
 984                 EMPTY_COMPLETION_PROVIDER,
 985                 CommandKind.NORMAL));
 986 
 987         // Documentation pseudo-commands
 988         registerCommand(new Command("/<id>",
 989                 "help.id",
 990                 CommandKind.HELP_ONLY));
 991         registerCommand(new Command("/-<n>",
 992                 "help.previous",
 993                 CommandKind.HELP_ONLY));
 994         registerCommand(new Command("intro",
 995                 "help.intro",
 996                 CommandKind.HELP_SUBJECT));
 997         registerCommand(new Command("shortcuts",
 998                 "help.shortcuts",
 999                 CommandKind.HELP_SUBJECT));
1000     }
1001 
1002     public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
1003         String prefix = code.substring(0, cursor);
1004         int space = prefix.indexOf(' ');
1005         Stream<Suggestion> result;
1006 
1007         if (space == (-1)) {
1008             result = commands.values()
1009                              .stream()
1010                              .distinct()
1011                              .filter(cmd -> cmd.kind.shouldSuggestCompletions)
1012                              .map(cmd -> cmd.command)
1013                              .filter(key -> key.startsWith(prefix))
1014                              .map(key -> new Suggestion(key + " ", false));
1015             anchor[0] = 0;
1016         } else {
1017             String arg = prefix.substring(space + 1);
1018             String cmd = prefix.substring(0, space);
1019             Command[] candidates = findCommand(cmd, c -> true);
1020             if (candidates.length == 1) {
1021                 result = candidates[0].completions.completionSuggestions(arg, cursor - space, anchor).stream();
1022                 anchor[0] += space + 1;
1023             } else {
1024                 result = Stream.empty();
1025             }
1026         }
1027 
1028         return result.sorted((s1, s2) -> s1.continuation.compareTo(s2.continuation))
1029                      .collect(Collectors.toList());
1030     }
1031 
1032     public String commandDocumentation(String code, int cursor) {
1033         code = code.substring(0, cursor);
1034         int space = code.indexOf(' ');
1035 
1036         if (space != (-1)) {
1037             String cmd = code.substring(0, space);
1038             Command command = commands.get(cmd);
1039             if (command != null) {
1040                 return getResourceString(command.helpKey + ".summary");
1041             }
1042         }
1043 
1044         return null;
1045     }
1046 
1047     // --- Command implementations ---
1048 
1049     private static final String[] SET_SUBCOMMANDS = new String[]{
1050         "format", "feedback", "newmode", "prompt", "editor", "start"};
1051 
1052     final boolean cmdSet(String arg) {
1053         ArgTokenizer at = new ArgTokenizer(arg.trim());
1054         String which = setSubCommand(at);
1055         if (which == null) {
1056             return false;
1057         }
1058         switch (which) {
1059             case "format":
1060                 return feedback.setFormat(this, at);
1061             case "feedback":
1062                 return feedback.setFeedback(this, at);
1063             case "newmode":
1064                 return feedback.setNewMode(this, at);
1065             case "prompt":
1066                 return feedback.setPrompt(this, at);
1067             case "editor": {
1068                 String prog = at.next();
1069                 if (prog == null) {
1070                     errormsg("jshell.err.set.editor.arg");
1071                     return false;
1072                 } else {
1073                     List<String> ed = new ArrayList<>();
1074                     ed.add(prog);
1075                     String n;
1076                     while ((n = at.next()) != null) {
1077                         ed.add(n);
1078                     }
1079                     editor = ed.toArray(new String[ed.size()]);
1080                     fluffmsg("jshell.msg.set.editor.set", prog);
1081                     return true;
1082                 }
1083             }
1084             case "start": {
1085                 String init = readFile(at.next(), "/set start");
1086                 if (init == null) {
1087                     return false;
1088                 } else {
1089                     prefs.put(STARTUP_KEY, init);
1090                     return true;
1091                 }
1092             }
1093             default:
1094                 errormsg("jshell.err.arg", "/set", at.val());
1095                 return false;
1096         }
1097     }
1098 
1099     boolean printSetHelp(ArgTokenizer at) {
1100         String which = setSubCommand(at);
1101         if (which == null) {
1102             return false;
1103         }
1104         hardrb("help.set." + which);
1105         return true;
1106     }
1107 
1108     String setSubCommand(ArgTokenizer at) {
1109         String[] matches = at.next(SET_SUBCOMMANDS);
1110         if (matches == null) {
1111             errormsg("jshell.err.set.arg");
1112             return null;
1113         }
1114         if (matches.length == 0) {
1115             errormsg("jshell.err.arg", "/set", at.val());
1116             fluffmsg("jshell.msg.use.one.of", Arrays.stream(SET_SUBCOMMANDS)
1117                     .collect(Collectors.joining(", "))
1118             );
1119             return null;
1120         }
1121         if (matches.length > 1) {
1122             errormsg("jshell.err.set.ambiguous", at.val());
1123             fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches)
1124                     .collect(Collectors.joining(", "))
1125             );
1126             return null;
1127         }
1128         return matches[0];
1129     }
1130 
1131     boolean cmdClasspath(String arg) {
1132         if (arg.isEmpty()) {
1133             errormsg("jshell.err.classpath.arg");
1134             return false;
1135         } else {
1136             state.addToClasspath(toPathResolvingUserHome(arg).toString());
1137             fluffmsg("jshell.msg.classpath", arg);
1138             return true;
1139         }
1140     }
1141 
1142     boolean cmdDebug(String arg) {
1143         if (arg.isEmpty()) {
1144             debug = !debug;
1145             InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0);
1146             fluff("Debugging %s", debug ? "on" : "off");
1147         } else {
1148             int flags = 0;
1149             for (char ch : arg.toCharArray()) {
1150                 switch (ch) {
1151                     case '0':
1152                         flags = 0;
1153                         debug = false;
1154                         fluff("Debugging off");
1155                         break;
1156                     case 'r':
1157                         debug = true;
1158                         fluff("REPL tool debugging on");
1159                         break;
1160                     case 'g':
1161                         flags |= InternalDebugControl.DBG_GEN;
1162                         fluff("General debugging on");
1163                         break;
1164                     case 'f':
1165                         flags |= InternalDebugControl.DBG_FMGR;
1166                         fluff("File manager debugging on");
1167                         break;
1168                     case 'c':
1169                         flags |= InternalDebugControl.DBG_COMPA;
1170                         fluff("Completion analysis debugging on");
1171                         break;
1172                     case 'd':
1173                         flags |= InternalDebugControl.DBG_DEP;
1174                         fluff("Dependency debugging on");
1175                         break;
1176                     case 'e':
1177                         flags |= InternalDebugControl.DBG_EVNT;
1178                         fluff("Event debugging on");
1179                         break;
1180                     default:
1181                         hard("Unknown debugging option: %c", ch);
1182                         fluff("Use: 0 r g f c d");
1183                         return false;
1184                 }
1185             }
1186             InternalDebugControl.setDebugFlags(state, flags);
1187         }
1188         return true;
1189     }
1190 
1191     private boolean cmdExit() {
1192         regenerateOnDeath = false;
1193         live = false;
1194         if (!replayableHistory.isEmpty()) {
1195             // Prevent history overflow by calculating what will fit, starting
1196             // with must recent
1197             int sepLen = RECORD_SEPARATOR.length();
1198             int length = 0;
1199             int first = replayableHistory.size();
1200             while(length < Preferences.MAX_VALUE_LENGTH && --first >= 0) {
1201                 length += replayableHistory.get(first).length() + sepLen;
1202             }
1203             String hist = replayableHistory
1204                     .subList(first + 1, replayableHistory.size())
1205                     .stream()
1206                     .reduce( (a, b) -> a + RECORD_SEPARATOR + b)
1207                     .getWhenPresent();
1208             prefs.put(REPLAY_RESTORE_KEY, hist);
1209         }
1210         fluffmsg("jshell.msg.goodbye");
1211         return true;
1212     }
1213 
1214     boolean cmdHelp(String arg) {
1215         ArgTokenizer at = new ArgTokenizer(arg);
1216         String subject = at.next();
1217         if (subject != null) {
1218             Command[] matches = commands.values().stream()
1219                     .filter(c -> c.command.startsWith(subject))
1220                     .toArray(size -> new Command[size]);
1221             at.mark();
1222             String sub = at.next();
1223             if (sub != null && matches.length == 1 && matches[0].command.equals("/set")) {
1224                 at.rewind();
1225                 return printSetHelp(at);
1226             }
1227             if (matches.length > 0) {
1228                 for (Command c : matches) {
1229                     hard("");
1230                     hard("%s", c.command);
1231                     hard("");
1232                     hardrb(c.helpKey);
1233                 }
1234                 return true;
1235             } else {
1236                 errormsg("jshell.err.help.arg", arg);
1237             }
1238         }
1239         hardmsg("jshell.msg.help.begin");
1240         hardPairs(commands.values().stream()
1241                 .filter(cmd -> cmd.kind.showInHelp),
1242                 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"),
1243                 cmd -> getResourceString(cmd.helpKey + ".summary")
1244         );
1245         hardmsg("jshell.msg.help.subject");
1246         hardPairs(commands.values().stream()
1247                 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT),
1248                 cmd -> cmd.command,
1249                 cmd -> getResourceString(cmd.helpKey + ".summary")
1250         );
1251         return true;
1252     }
1253 
1254     private boolean cmdHistory() {
1255         cmdout.println();
1256         for (String s : input.currentSessionHistory()) {
1257             // No number prefix, confusing with snippet ids
1258             cmdout.printf("%s\n", s);
1259         }
1260         return true;
1261     }
1262 
1263     /**
1264      * Avoid parameterized varargs possible heap pollution warning.
1265      */
1266     private interface SnippetPredicate extends Predicate<Snippet> { }
1267 
1268     /**
1269      * Apply filters to a stream until one that is non-empty is found.
1270      * Adapted from Stuart Marks
1271      *
1272      * @param supplier Supply the Snippet stream to filter
1273      * @param filters Filters to attempt
1274      * @return The non-empty filtered Stream, or null
1275      */
1276     private static Stream<Snippet> nonEmptyStream(Supplier<Stream<Snippet>> supplier,
1277             SnippetPredicate... filters) {
1278         for (SnippetPredicate filt : filters) {
1279             Iterator<Snippet> iterator = supplier.get().filter(filt).iterator();
1280             if (iterator.hasNext()) {
1281                 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
1282             }
1283         }
1284         return null;
1285     }
1286 
1287     private boolean inStartUp(Snippet sn) {
1288         return mapSnippet.get(sn).space == startNamespace;
1289     }
1290 
1291     private boolean isActive(Snippet sn) {
1292         return state.status(sn).isActive;
1293     }
1294 
1295     private boolean mainActive(Snippet sn) {
1296         return !inStartUp(sn) && isActive(sn);
1297     }
1298 
1299     private boolean matchingDeclaration(Snippet sn, String name) {
1300         return sn instanceof DeclarationSnippet
1301                 && ((DeclarationSnippet) sn).name().equals(name);
1302     }
1303 
1304     /**
1305      * Convert a user argument to a Stream of snippets referenced by that argument
1306      * (or lack of argument).
1307      *
1308      * @param arg The user's argument to the command, maybe be the empty string
1309      * @return a Stream of referenced snippets or null if no matches to specific arg
1310      */
1311     private Stream<Snippet> argToSnippets(String arg, boolean allowAll) {
1312         List<Snippet> snippets = state.snippets();
1313         if (allowAll && arg.equals("all")) {
1314             // all snippets including start-up, failed, and overwritten
1315             return snippets.stream();
1316         } else if (allowAll && arg.equals("start")) {
1317             // start-up snippets
1318             return snippets.stream()
1319                     .filter(this::inStartUp);
1320         } else if (arg.isEmpty()) {
1321             // Default is all active user snippets
1322             return snippets.stream()
1323                     .filter(this::mainActive);
1324         } else {
1325             Stream<Snippet> result =
1326                     nonEmptyStream(
1327                             () -> snippets.stream(),
1328                             // look for active user declarations matching the name
1329                             sn -> isActive(sn) && matchingDeclaration(sn, arg),
1330                             // else, look for any declarations matching the name
1331                             sn -> matchingDeclaration(sn, arg),
1332                             // else, look for an id of this name
1333                             sn -> sn.id().equals(arg)
1334                     );
1335             return result;
1336         }
1337     }
1338 
1339     private boolean cmdDrop(String arg) {
1340         if (arg.isEmpty()) {
1341             errormsg("jshell.err.drop.arg");
1342             return false;
1343         }
1344         Stream<Snippet> stream = argToSnippets(arg, false);
1345         if (stream == null) {
1346             errormsg("jshell.err.def.or.id.not.found", arg);
1347             fluffmsg("jshell.msg.see.classes.etc");
1348             return false;
1349         }
1350         List<Snippet> snippets = stream
1351                 .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet)
1352                 .collect(toList());
1353         if (snippets.isEmpty()) {
1354             errormsg("jshell.err.drop.not.active");
1355             return false;
1356         }
1357         if (snippets.size() > 1) {
1358             errormsg("jshell.err.drop.ambiguous");
1359             fluffmsg("jshell.msg.use.one.of", snippets.stream()
1360                     .map(sn -> String.format("\n%4s : %s", sn.id(), sn.source().replace("\n", "\n       ")))
1361                     .collect(Collectors.joining(", "))
1362             );
1363             return false;
1364         }
1365         PersistentSnippet psn = (PersistentSnippet) snippets.get(0);
1366         state.drop(psn).forEach(this::handleEvent);
1367         return true;
1368     }
1369 
1370     private boolean cmdEdit(String arg) {
1371         Stream<Snippet> stream = argToSnippets(arg, true);
1372         if (stream == null) {
1373             errormsg("jshell.err.def.or.id.not.found", arg);
1374             fluffmsg("jshell.msg.see.classes.etc");
1375             return false;
1376         }
1377         Set<String> srcSet = new LinkedHashSet<>();
1378         stream.forEachOrdered(sn -> {
1379             String src = sn.source();
1380             switch (sn.subKind()) {
1381                 case VAR_VALUE_SUBKIND:
1382                     break;
1383                 case ASSIGNMENT_SUBKIND:
1384                 case OTHER_EXPRESSION_SUBKIND:
1385                 case TEMP_VAR_EXPRESSION_SUBKIND:
1386                     if (!src.endsWith(";")) {
1387                         src = src + ";";
1388                     }
1389                     srcSet.add(src);
1390                     break;
1391                 default:
1392                     srcSet.add(src);
1393                     break;
1394             }
1395         });
1396         StringBuilder sb = new StringBuilder();
1397         for (String s : srcSet) {
1398             sb.append(s);
1399             sb.append('\n');
1400         }
1401         String src = sb.toString();
1402         Consumer<String> saveHandler = new SaveHandler(src, srcSet);
1403         Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
1404         if (editor == null) {
1405             try {
1406                 EditPad.edit(errorHandler, src, saveHandler);
1407             } catch (RuntimeException ex) {
1408                 errormsg("jshell.err.cant.launch.editor", ex);
1409                 fluffmsg("jshell.msg.try.set.editor");
1410                 return false;
1411             }
1412         } else {
1413             ExternalEditor.edit(editor, errorHandler, src, saveHandler, input);
1414         }
1415         return true;
1416     }
1417     //where
1418     // receives editor requests to save
1419     private class SaveHandler implements Consumer<String> {
1420 
1421         String src;
1422         Set<String> currSrcs;
1423 
1424         SaveHandler(String src, Set<String> ss) {
1425             this.src = src;
1426             this.currSrcs = ss;
1427         }
1428 
1429         @Override
1430         public void accept(String s) {
1431             if (!s.equals(src)) { // quick check first
1432                 src = s;
1433                 try {
1434                     Set<String> nextSrcs = new LinkedHashSet<>();
1435                     boolean failed = false;
1436                     while (true) {
1437                         CompletionInfo an = analysis.analyzeCompletion(s);
1438                         if (!an.completeness.isComplete) {
1439                             break;
1440                         }
1441                         String tsrc = trimNewlines(an.source);
1442                         if (!failed && !currSrcs.contains(tsrc)) {
1443                             failed = processCompleteSource(tsrc);
1444                         }
1445                         nextSrcs.add(tsrc);
1446                         if (an.remaining.isEmpty()) {
1447                             break;
1448                         }
1449                         s = an.remaining;
1450                     }
1451                     currSrcs = nextSrcs;
1452                 } catch (IllegalStateException ex) {
1453                     hardmsg("jshell.msg.resetting");
1454                     resetState();
1455                     currSrcs = new LinkedHashSet<>(); // re-process everything
1456                 }
1457             }
1458         }
1459 
1460         private String trimNewlines(String s) {
1461             int b = 0;
1462             while (b < s.length() && s.charAt(b) == '\n') {
1463                 ++b;
1464             }
1465             int e = s.length() -1;
1466             while (e >= 0 && s.charAt(e) == '\n') {
1467                 --e;
1468             }
1469             return s.substring(b, e + 1);
1470         }
1471     }
1472 
1473     private boolean cmdList(String arg) {
1474         if (arg.equals("history")) {
1475             return cmdHistory();
1476         }
1477         Stream<Snippet> stream = argToSnippets(arg, true);
1478         if (stream == null) {
1479             errormsg("jshell.err.def.or.id.not.found", arg);
1480             // Check if there are any definitions at all
1481             if (argToSnippets("", false).iterator().hasNext()) {
1482                 fluffmsg("jshell.msg.try.list.without.args");
1483             } else {
1484                 hardmsg("jshell.msg.no.active");
1485             }
1486             return false;
1487         }
1488 
1489         // prevent double newline on empty list
1490         boolean[] hasOutput = new boolean[1];
1491         stream.forEachOrdered(sn -> {
1492             if (!hasOutput[0]) {
1493                 cmdout.println();
1494                 hasOutput[0] = true;
1495             }
1496             cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n       "));
1497         });
1498         return true;
1499     }
1500 
1501     private boolean cmdOpen(String filename) {
1502         return runFile(filename, "/open");
1503     }
1504 
1505     private boolean runFile(String filename, String context) {
1506         if (!filename.isEmpty()) {
1507             try {
1508                 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString()));
1509                 return true;
1510             } catch (FileNotFoundException e) {
1511                 errormsg("jshell.err.file.not.found", context, filename, e.getMessage());
1512             } catch (Exception e) {
1513                 errormsg("jshell.err.file.exception", context, filename, e);
1514             }
1515         } else {
1516             errormsg("jshell.err.file.filename", context);
1517         }
1518         return false;
1519     }
1520 
1521     /**
1522      * Read an external file. Error messages accessed via keyPrefix
1523      *
1524      * @param filename file to access or null
1525      * @param context printable non-natural language context for errors
1526      * @return contents of file as string
1527      */
1528     String readFile(String filename, String context) {
1529         if (filename != null) {
1530             try {
1531                 byte[] encoded = Files.readAllBytes(Paths.get(filename));
1532                 return new String(encoded);
1533             } catch (AccessDeniedException e) {
1534                 errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage());
1535             } catch (NoSuchFileException e) {
1536                 errormsg("jshell.err.file.not.found", context, filename);
1537             } catch (Exception e) {
1538                 errormsg("jshell.err.file.exception", context, filename, e);
1539             }
1540         } else {
1541             errormsg("jshell.err.file.filename", context);
1542         }
1543         return null;
1544 
1545     }
1546 
1547     private boolean cmdReset() {
1548         live = false;
1549         fluffmsg("jshell.msg.resetting.state");
1550         return true;
1551     }
1552 
1553     private boolean cmdReload(String arg) {
1554         Iterable<String> history = replayableHistory;
1555         boolean echo = true;
1556         if (arg.length() > 0) {
1557             if ("restore".startsWith(arg)) {
1558                 if (replayableHistoryPrevious == null) {
1559                     errormsg("jshell.err.reload.no.previous");
1560                     return false;
1561                 }
1562                 history = replayableHistoryPrevious;
1563             } else if ("quiet".startsWith(arg)) {
1564                 echo = false;
1565             } else {
1566                 errormsg("jshell.err.arg", "/reload", arg);
1567                 return false;
1568             }
1569         }
1570         fluffmsg(history == replayableHistoryPrevious
1571                         ? "jshell.err.reload.restarting.previous.state"
1572                         : "jshell.err.reload.restarting.state");
1573         resetState();
1574         run(new ReloadIOContext(history,
1575                 echo? cmdout : null));
1576         return true;
1577     }
1578 
1579     private boolean cmdSave(String arg_filename) {
1580         Matcher mat = HISTORY_ALL_START_FILENAME.matcher(arg_filename);
1581         if (!mat.find()) {
1582             errormsg("jshell.err.arg", arg_filename);
1583             return false;
1584         }
1585         boolean useHistory = false;
1586         String saveAll = "";
1587         boolean saveStart = false;
1588         String cmd = mat.group("cmd");
1589         if (cmd != null) switch (cmd) {
1590             case "all":
1591                 saveAll = "all";
1592                 break;
1593             case "history":
1594                 useHistory = true;
1595                 break;
1596             case "start":
1597                 saveStart = true;
1598                 break;
1599         }
1600         String filename = mat.group("filename");
1601         if (filename == null ||filename.isEmpty()) {
1602             errormsg("jshell.err.file.filename", "/save");
1603             return false;
1604         }
1605         try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
1606                 Charset.defaultCharset(),
1607                 CREATE, TRUNCATE_EXISTING, WRITE)) {
1608             if (useHistory) {
1609                 for (String s : input.currentSessionHistory()) {
1610                     writer.write(s);
1611                     writer.write("\n");
1612                 }
1613             } else if (saveStart) {
1614                 writer.append(DEFAULT_STARTUP);
1615             } else {
1616                 Stream<Snippet> stream = argToSnippets(saveAll, true);
1617                 if (stream != null) {
1618                     for (Snippet sn : stream.collect(toList())) {
1619                         writer.write(sn.source());
1620                         writer.write("\n");
1621                     }
1622                 }
1623             }
1624         } catch (FileNotFoundException e) {
1625             errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
1626             return false;
1627         } catch (Exception e) {
1628             errormsg("jshell.err.file.exception", "/save", filename, e);
1629             return false;
1630         }
1631         return true;
1632     }
1633 
1634     private boolean cmdVars() {
1635         for (VarSnippet vk : state.variables()) {
1636             String val = state.status(vk) == Status.VALID
1637                     ? state.varValue(vk)
1638                     : "jshell.msg.vars.not.active";
1639             hard("  %s %s = %s", vk.typeName(), vk.name(), val);
1640         }
1641         return true;
1642     }
1643 
1644     private boolean cmdMethods() {
1645         for (MethodSnippet mk : state.methods()) {
1646             hard("  %s %s", mk.name(), mk.signature());
1647         }
1648         return true;
1649     }
1650 
1651     private boolean cmdClasses() {
1652         for (TypeDeclSnippet ck : state.types()) {
1653             String kind;
1654             switch (ck.subKind()) {
1655                 case INTERFACE_SUBKIND:
1656                     kind = "interface";
1657                     break;
1658                 case CLASS_SUBKIND:
1659                     kind = "class";
1660                     break;
1661                 case ENUM_SUBKIND:
1662                     kind = "enum";
1663                     break;
1664                 case ANNOTATION_TYPE_SUBKIND:
1665                     kind = "@interface";
1666                     break;
1667                 default:
1668                     assert false : "Wrong kind" + ck.subKind();
1669                     kind = "class";
1670                     break;
1671             }
1672             hard("  %s %s", kind, ck.name());
1673         }
1674         return true;
1675     }
1676 
1677     private boolean cmdImports() {
1678         state.imports().forEach(ik -> {
1679             hard("  import %s%s", ik.isStatic() ? "static " : "", ik.fullname());
1680         });
1681         return true;
1682     }
1683 
1684     private boolean cmdUseHistoryEntry(int index) {
1685         List<Snippet> keys = state.snippets();
1686         if (index < 0)
1687             index += keys.size();
1688         else
1689             index--;
1690         if (index >= 0 && index < keys.size()) {
1691             rerunSnippet(keys.get(index));
1692         } else {
1693             errormsg("jshell.err.out.of.range");
1694             return false;
1695         }
1696         return true;
1697     }
1698 
1699     private boolean rerunHistoryEntryById(String id) {
1700         Optional<Snippet> snippet = state.snippets().stream()
1701             .filter(s -> s.id().equals(id))
1702             .findFirst();
1703         return snippet.map(s -> {
1704             rerunSnippet(s);
1705             return true;
1706         }).orElse(false);
1707     }
1708 
1709     private void rerunSnippet(Snippet snippet) {
1710         String source = snippet.source();
1711         cmdout.printf("%s\n", source);
1712         input.replaceLastHistoryEntry(source);
1713         processSourceCatchingReset(source);
1714     }
1715 
1716     /**
1717      * Filter diagnostics for only errors (no warnings, ...)
1718      * @param diagnostics input list
1719      * @return filtered list
1720      */
1721     List<Diag> errorsOnly(List<Diag> diagnostics) {
1722         return diagnostics.stream()
1723                 .filter(d -> d.isError())
1724                 .collect(toList());
1725     }
1726 
1727     void displayDiagnostics(String source, Diag diag, List<String> toDisplay) {
1728         for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize
1729             if (!line.trim().startsWith("location:")) {
1730                 toDisplay.add(line);
1731             }
1732         }
1733 
1734         int pstart = (int) diag.getStartPosition();
1735         int pend = (int) diag.getEndPosition();
1736         Matcher m = LINEBREAK.matcher(source);
1737         int pstartl = 0;
1738         int pendl = -2;
1739         while (m.find(pstartl)) {
1740             pendl = m.start();
1741             if (pendl >= pstart) {
1742                 break;
1743             } else {
1744                 pstartl = m.end();
1745             }
1746         }
1747         if (pendl < pstart) {
1748             pendl = source.length();
1749         }
1750         toDisplay.add(source.substring(pstartl, pendl));
1751 
1752         StringBuilder sb = new StringBuilder();
1753         int start = pstart - pstartl;
1754         for (int i = 0; i < start; ++i) {
1755             sb.append(' ');
1756         }
1757         sb.append('^');
1758         boolean multiline = pend > pendl;
1759         int end = (multiline ? pendl : pend) - pstartl - 1;
1760         if (end > start) {
1761             for (int i = start + 1; i < end; ++i) {
1762                 sb.append('-');
1763             }
1764             if (multiline) {
1765                 sb.append("-...");
1766             } else {
1767                 sb.append('^');
1768             }
1769         }
1770         toDisplay.add(sb.toString());
1771 
1772         debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
1773         debug("Code: %s", diag.getCode());
1774         debug("Pos: %d (%d - %d)", diag.getPosition(),
1775                 diag.getStartPosition(), diag.getEndPosition());
1776     }
1777 
1778     private String processSource(String srcInput) throws IllegalStateException {
1779         while (true) {
1780             CompletionInfo an = analysis.analyzeCompletion(srcInput);
1781             if (!an.completeness.isComplete) {
1782                 return an.remaining;
1783             }
1784             boolean failed = processCompleteSource(an.source);
1785             if (failed || an.remaining.isEmpty()) {
1786                 return "";
1787             }
1788             srcInput = an.remaining;
1789         }
1790     }
1791     //where
1792     private boolean processCompleteSource(String source) throws IllegalStateException {
1793         debug("Compiling: %s", source);
1794         boolean failed = false;
1795         boolean isActive = false;
1796         List<SnippetEvent> events = state.eval(source);
1797         for (SnippetEvent e : events) {
1798             // Report the event, recording failure
1799             failed |= handleEvent(e);
1800 
1801             // If any main snippet is active, this should be replayable
1802             // also ignore var value queries
1803             isActive |= e.causeSnippet() == null &&
1804                     e.status().isActive &&
1805                     e.snippet().subKind() != VAR_VALUE_SUBKIND;
1806         }
1807         // If this is an active snippet and it didn't cause the backend to die,
1808         // add it to the replayable history
1809         if (isActive && live) {
1810             addToReplayHistory(source);
1811         }
1812 
1813         return failed;
1814     }
1815 
1816     // Handle incoming snippet events -- return true on failure
1817     private boolean handleEvent(SnippetEvent ste) {
1818         Snippet sn = ste.snippet();
1819         if (sn == null) {
1820             debug("Event with null key: %s", ste);
1821             return false;
1822         }
1823         List<Diag> diagnostics = state.diagnostics(sn);
1824         String source = sn.source();
1825         if (ste.causeSnippet() == null) {
1826             // main event
1827             for (Diag d : diagnostics) {
1828                 hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning");
1829                 List<String> disp = new ArrayList<>();
1830                 displayDiagnostics(source, d, disp);
1831                 disp.stream()
1832                         .forEach(l -> hard(l));
1833             }
1834 
1835             if (ste.status() != Status.REJECTED) {
1836                 if (ste.exception() != null) {
1837                     if (ste.exception() instanceof EvalException) {
1838                         printEvalException((EvalException) ste.exception());
1839                         return true;
1840                     } else if (ste.exception() instanceof UnresolvedReferenceException) {
1841                         printUnresolvedException((UnresolvedReferenceException) ste.exception());
1842                     } else {
1843                         hard("Unexpected execution exception: %s", ste.exception());
1844                         return true;
1845                     }
1846                 } else {
1847                     new DisplayEvent(ste, false, ste.value(), diagnostics).displayDeclarationAndValue();
1848                 }
1849             } else {
1850                 if (diagnostics.isEmpty()) {
1851                     errormsg("jshell.err.failed");
1852                 }
1853                 return true;
1854             }
1855         } else {
1856             // Update
1857             if (sn instanceof DeclarationSnippet) {
1858                 List<Diag> other = errorsOnly(diagnostics);
1859 
1860                 // display update information
1861                 new DisplayEvent(ste, true, ste.value(), other).displayDeclarationAndValue();
1862             }
1863         }
1864         return false;
1865     }
1866     //where
1867     void printStackTrace(StackTraceElement[] stes) {
1868         for (StackTraceElement ste : stes) {
1869             StringBuilder sb = new StringBuilder();
1870             String cn = ste.getClassName();
1871             if (!cn.isEmpty()) {
1872                 int dot = cn.lastIndexOf('.');
1873                 if (dot > 0) {
1874                     sb.append(cn.substring(dot + 1));
1875                 } else {
1876                     sb.append(cn);
1877                 }
1878                 sb.append(".");
1879             }
1880             if (!ste.getMethodName().isEmpty()) {
1881                 sb.append(ste.getMethodName());
1882                 sb.append(" ");
1883             }
1884             String fileName = ste.getFileName();
1885             int lineNumber = ste.getLineNumber();
1886             String loc = ste.isNativeMethod()
1887                     ? getResourceString("jshell.msg.native.method")
1888                     : fileName == null
1889                             ? getResourceString("jshell.msg.unknown.source")
1890                             : lineNumber >= 0
1891                                     ? fileName + ":" + lineNumber
1892                                     : fileName;
1893             hard("      at %s(%s)", sb, loc);
1894 
1895         }
1896     }
1897     //where
1898     void printUnresolvedException(UnresolvedReferenceException ex) {
1899         DeclarationSnippet corralled =  ex.getSnippet();
1900         List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled));
1901         new DisplayEvent(corralled, state.status(corralled), FormatAction.USED, true, null, otherErrors)
1902                 .displayDeclarationAndValue();
1903     }
1904     //where
1905     void printEvalException(EvalException ex) {
1906         if (ex.getMessage() == null) {
1907             hard("%s thrown", ex.getExceptionClassName());
1908         } else {
1909             hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
1910         }
1911         printStackTrace(ex.getStackTrace());
1912     }
1913 
1914     private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) {
1915         FormatAction act;
1916         switch (status) {
1917             case VALID:
1918             case RECOVERABLE_DEFINED:
1919             case RECOVERABLE_NOT_DEFINED:
1920                 if (previousStatus.isActive) {
1921                     act = isSignatureChange
1922                             ? FormatAction.REPLACED
1923                             : FormatAction.MODIFIED;
1924                 } else {
1925                     act = FormatAction.ADDED;
1926                 }
1927                 break;
1928             case OVERWRITTEN:
1929                 act = FormatAction.OVERWROTE;
1930                 break;
1931             case DROPPED:
1932                 act = FormatAction.DROPPED;
1933                 break;
1934             case REJECTED:
1935             case NONEXISTENT:
1936             default:
1937                 // Should not occur
1938                 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString());
1939                 act = FormatAction.DROPPED;
1940         }
1941         return act;
1942     }
1943 
1944     class DisplayEvent {
1945         private final Snippet sn;
1946         private final FormatAction action;
1947         private final boolean update;
1948         private final String value;
1949         private final List<String> errorLines;
1950         private final FormatResolve resolution;
1951         private final String unresolved;
1952         private final FormatUnresolved unrcnt;
1953         private final FormatErrors errcnt;
1954 
1955         DisplayEvent(SnippetEvent ste, boolean update, String value, List<Diag> errors) {
1956             this(ste.snippet(), ste.status(), toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors);
1957         }
1958 
1959         DisplayEvent(Snippet sn, Status status, FormatAction action, boolean update, String value, List<Diag> errors) {
1960             this.sn = sn;
1961             this.action = action;
1962             this.update = update;
1963             this.value = value;
1964             this.errorLines = new ArrayList<>();
1965             for (Diag d : errors) {
1966                 displayDiagnostics(sn.source(), d, errorLines);
1967             }
1968             int unresolvedCount;
1969             if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) {
1970                 resolution = (status == Status.RECOVERABLE_NOT_DEFINED)
1971                         ? FormatResolve.NOTDEFINED
1972                         : FormatResolve.DEFINED;
1973                 unresolved = unresolved((DeclarationSnippet) sn);
1974                 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).size();
1975             } else {
1976                 resolution = FormatResolve.OK;
1977                 unresolved = "";
1978                 unresolvedCount = 0;
1979             }
1980             unrcnt = unresolvedCount == 0
1981                     ? FormatUnresolved.UNRESOLVED0
1982                     : unresolvedCount == 1
1983                         ? FormatUnresolved.UNRESOLVED1
1984                         : FormatUnresolved.UNRESOLVED2;
1985             errcnt = errors.isEmpty()
1986                     ? FormatErrors.ERROR0
1987                     : errors.size() == 1
1988                         ? FormatErrors.ERROR1
1989                         : FormatErrors.ERROR2;
1990         }
1991 
1992         private String unresolved(DeclarationSnippet key) {
1993             List<String> unr = state.unresolvedDependencies(key);
1994             StringBuilder sb = new StringBuilder();
1995             int fromLast = unr.size();
1996             if (fromLast > 0) {
1997                 sb.append(" ");
1998             }
1999             for (String u : unr) {
2000                 --fromLast;
2001                 sb.append(u);
2002                 switch (fromLast) {
2003                     // No suffix
2004                     case 0:
2005                         break;
2006                     case 1:
2007                         sb.append(", and ");
2008                         break;
2009                     default:
2010                         sb.append(", ");
2011                         break;
2012                 }
2013             }
2014             return sb.toString();
2015         }
2016 
2017         private void custom(FormatCase fcase, String name) {
2018             custom(fcase, name, null);
2019         }
2020 
2021         private void custom(FormatCase fcase, String name, String type) {
2022             String display = feedback.format(fcase, action, (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY),
2023                     resolution, unrcnt, errcnt,
2024                     name, type, value, unresolved, errorLines);
2025             if (interactive()) {
2026                 cmdout.print(display);
2027             }
2028         }
2029 
2030         @SuppressWarnings("fallthrough")
2031         private void displayDeclarationAndValue() {
2032             switch (sn.subKind()) {
2033                 case CLASS_SUBKIND:
2034                     custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name());
2035                     break;
2036                 case INTERFACE_SUBKIND:
2037                     custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name());
2038                     break;
2039                 case ENUM_SUBKIND:
2040                     custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name());
2041                     break;
2042                 case ANNOTATION_TYPE_SUBKIND:
2043                     custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name());
2044                     break;
2045                 case METHOD_SUBKIND:
2046                     custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes());
2047                     break;
2048                 case VAR_DECLARATION_SUBKIND: {
2049                     VarSnippet vk = (VarSnippet) sn;
2050                     custom(FormatCase.VARDECL, vk.name(), vk.typeName());
2051                     break;
2052                 }
2053                 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: {
2054                     VarSnippet vk = (VarSnippet) sn;
2055                     custom(FormatCase.VARINIT, vk.name(), vk.typeName());
2056                     break;
2057                 }
2058                 case TEMP_VAR_EXPRESSION_SUBKIND: {
2059                     VarSnippet vk = (VarSnippet) sn;
2060                     custom(FormatCase.EXPRESSION, vk.name(), vk.typeName());
2061                     break;
2062                 }
2063                 case OTHER_EXPRESSION_SUBKIND:
2064                     error("Unexpected expression form -- value is: %s", (value));
2065                     break;
2066                 case VAR_VALUE_SUBKIND: {
2067                     ExpressionSnippet ek = (ExpressionSnippet) sn;
2068                     custom(FormatCase.VARVALUE, ek.name(), ek.typeName());
2069                     break;
2070                 }
2071                 case ASSIGNMENT_SUBKIND: {
2072                     ExpressionSnippet ek = (ExpressionSnippet) sn;
2073                     custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName());
2074                     break;
2075                 }
2076                 case SINGLE_TYPE_IMPORT_SUBKIND:
2077                 case TYPE_IMPORT_ON_DEMAND_SUBKIND:
2078                 case SINGLE_STATIC_IMPORT_SUBKIND:
2079                 case STATIC_IMPORT_ON_DEMAND_SUBKIND:
2080                     custom(FormatCase.IMPORT, ((ImportSnippet) sn).name());
2081                     break;
2082                 case STATEMENT_SUBKIND:
2083                     custom(FormatCase.STATEMENT, null);
2084                     break;
2085             }
2086         }
2087     }
2088 
2089     /** The current version number as a string.
2090      */
2091     String version() {
2092         return version("release");  // mm.nn.oo[-milestone]
2093     }
2094 
2095     /** The current full version number as a string.
2096      */
2097     String fullVersion() {
2098         return version("full"); // mm.mm.oo[-milestone]-build
2099     }
2100 
2101     private String version(String key) {
2102         if (versionRB == null) {
2103             try {
2104                 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale);
2105             } catch (MissingResourceException e) {
2106                 return "(version info not available)";
2107             }
2108         }
2109         try {
2110             return versionRB.getString(key);
2111         }
2112         catch (MissingResourceException e) {
2113             return "(version info not available)";
2114         }
2115     }
2116 
2117     class NameSpace {
2118         final String spaceName;
2119         final String prefix;
2120         private int nextNum;
2121 
2122         NameSpace(String spaceName, String prefix) {
2123             this.spaceName = spaceName;
2124             this.prefix = prefix;
2125             this.nextNum = 1;
2126         }
2127 
2128         String tid(Snippet sn) {
2129             String tid = prefix + nextNum++;
2130             mapSnippet.put(sn, new SnippetInfo(sn, this, tid));
2131             return tid;
2132         }
2133 
2134         String tidNext() {
2135             return prefix + nextNum;
2136         }
2137     }
2138 
2139     static class SnippetInfo {
2140         final Snippet snippet;
2141         final NameSpace space;
2142         final String tid;
2143 
2144         SnippetInfo(Snippet snippet, NameSpace space, String tid) {
2145             this.snippet = snippet;
2146             this.space = space;
2147             this.tid = tid;
2148         }
2149     }
2150 }
2151 
2152 abstract class NonInteractiveIOContext extends IOContext {
2153 
2154     @Override
2155     public boolean interactiveOutput() {
2156         return false;
2157     }
2158 
2159     @Override
2160     public Iterable<String> currentSessionHistory() {
2161         return Collections.emptyList();
2162     }
2163 
2164     @Override
2165     public boolean terminalEditorRunning() {
2166         return false;
2167     }
2168 
2169     @Override
2170     public void suspend() {
2171     }
2172 
2173     @Override
2174     public void resume() {
2175     }
2176 
2177     @Override
2178     public void beforeUserCode() {
2179     }
2180 
2181     @Override
2182     public void afterUserCode() {
2183     }
2184 
2185     @Override
2186     public void replaceLastHistoryEntry(String source) {
2187     }
2188 }
2189 
2190 class ScannerIOContext extends NonInteractiveIOContext {
2191     private final Scanner scannerIn;
2192 
2193     ScannerIOContext(Scanner scannerIn) {
2194         this.scannerIn = scannerIn;
2195     }
2196 
2197     @Override
2198     public String readLine(String prompt, String prefix) {
2199         if (scannerIn.hasNextLine()) {
2200             return scannerIn.nextLine();
2201         } else {
2202             return null;
2203         }
2204     }
2205 
2206     @Override
2207     public void close() {
2208         scannerIn.close();
2209     }
2210 }
2211 
2212 class FileScannerIOContext extends ScannerIOContext {
2213 
2214     FileScannerIOContext(String fn) throws FileNotFoundException {
2215         this(new FileReader(fn));
2216     }
2217 
2218     FileScannerIOContext(Reader rdr) throws FileNotFoundException {
2219         super(new Scanner(rdr));
2220     }
2221 }
2222 
2223 class ReloadIOContext extends NonInteractiveIOContext {
2224     private final Iterator<String> it;
2225     private final PrintStream echoStream;
2226 
2227     ReloadIOContext(Iterable<String> history, PrintStream echoStream) {
2228         this.it = history.iterator();
2229         this.echoStream = echoStream;
2230     }
2231 
2232     @Override
2233     public String readLine(String prompt, String prefix) {
2234         String s = it.hasNext()
2235                 ? it.next()
2236                 : null;
2237         if (echoStream != null && s != null) {
2238             String p = "-: ";
2239             String p2 = "\n   ";
2240             echoStream.printf("%s%s\n", p, s.replace("\n", p2));
2241         }
2242         return s;
2243     }
2244 
2245     @Override
2246     public void close() {
2247     }
2248 }