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