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