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