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