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